diff --git a/.coveragerc b/.coveragerc index b88db04035a..1ccb9e461df 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,12 +1,17 @@ +# Sorted by hassfest. +# +# To sort, run python3 -m script.hassfest -p coverage + [run] source = homeassistant omit = homeassistant/__main__.py + homeassistant/helpers/backports/aiohttp_resolver.py homeassistant/helpers/signal.py homeassistant/scripts/__init__.py + homeassistant/scripts/benchmark/__init__.py homeassistant/scripts/check_config.py homeassistant/scripts/ensure_config.py - homeassistant/scripts/benchmark/__init__.py homeassistant/scripts/macos/__init__.py # omit pieces of code that rely on external devices being present @@ -103,10 +108,8 @@ omit = homeassistant/components/aurora/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py - homeassistant/components/azure_devops/__init__.py - homeassistant/components/azure_devops/sensor.py - homeassistant/components/azure_service_bus/* homeassistant/components/awair/coordinator.py + homeassistant/components/azure_service_bus/* homeassistant/components/baf/__init__.py homeassistant/components/baf/climate.py homeassistant/components/baf/entity.py @@ -190,8 +193,8 @@ omit = homeassistant/components/comelit/alarm_control_panel.py homeassistant/components/comelit/climate.py homeassistant/components/comelit/const.py - homeassistant/components/comelit/cover.py homeassistant/components/comelit/coordinator.py + homeassistant/components/comelit/cover.py homeassistant/components/comelit/humidifier.py homeassistant/components/comelit/light.py homeassistant/components/comelit/sensor.py @@ -239,8 +242,8 @@ omit = homeassistant/components/dominos/* homeassistant/components/doods/* homeassistant/components/doorbird/__init__.py - homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/button.py + homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/device.py homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/util.py @@ -260,12 +263,12 @@ omit = homeassistant/components/dunehd/__init__.py homeassistant/components/dunehd/media_player.py homeassistant/components/duotecno/__init__.py - homeassistant/components/duotecno/entity.py - homeassistant/components/duotecno/switch.py - homeassistant/components/duotecno/cover.py - homeassistant/components/duotecno/light.py - homeassistant/components/duotecno/climate.py homeassistant/components/duotecno/binary_sensor.py + homeassistant/components/duotecno/climate.py + homeassistant/components/duotecno/cover.py + homeassistant/components/duotecno/entity.py + homeassistant/components/duotecno/light.py + homeassistant/components/duotecno/switch.py homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/sensor.py @@ -305,10 +308,12 @@ omit = homeassistant/components/edl21/__init__.py homeassistant/components/edl21/sensor.py homeassistant/components/egardia/* + homeassistant/components/electrasmart/__init__.py + homeassistant/components/electrasmart/climate.py homeassistant/components/electric_kiwi/__init__.py homeassistant/components/electric_kiwi/api.py - homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/coordinator.py + homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/select.py homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py @@ -356,12 +361,18 @@ omit = homeassistant/components/environment_canada/weather.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epic_games_store/__init__.py + homeassistant/components/epic_games_store/coordinator.py homeassistant/components/epion/__init__.py homeassistant/components/epion/coordinator.py homeassistant/components/epion/sensor.py homeassistant/components/epson/__init__.py homeassistant/components/epson/media_player.py - homeassistant/components/epsonworkforce/sensor.py + homeassistant/components/eq3btsmart/__init__.py + homeassistant/components/eq3btsmart/climate.py + homeassistant/components/eq3btsmart/const.py + homeassistant/components/eq3btsmart/entity.py + homeassistant/components/eq3btsmart/models.py homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py @@ -376,11 +387,11 @@ omit = homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/button.py homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/coordinator.py + homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/image.py homeassistant/components/ezviz/light.py - homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/number.py - homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/select.py homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/siren.py @@ -529,16 +540,13 @@ omit = homeassistant/components/hive/switch.py homeassistant/components/hive/water_heater.py homeassistant/components/hko/__init__.py - homeassistant/components/hko/weather.py homeassistant/components/hko/coordinator.py + homeassistant/components/hko/weather.py homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/switch.py - homeassistant/components/home_connect/__init__.py - homeassistant/components/home_connect/api.py homeassistant/components/home_connect/binary_sensor.py homeassistant/components/home_connect/entity.py homeassistant/components/home_connect/light.py - homeassistant/components/home_connect/sensor.py homeassistant/components/home_connect/switch.py homeassistant/components/homematic/__init__.py homeassistant/components/homematic/binary_sensor.py @@ -568,9 +576,9 @@ omit = homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py homeassistant/components/hunterdouglas_powerview/util.py - homeassistant/components/hvv_departures/__init__.py homeassistant/components/huum/__init__.py homeassistant/components/huum/climate.py + homeassistant/components/hvv_departures/__init__.py homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py homeassistant/components/ialarm/alarm_control_panel.py @@ -663,9 +671,9 @@ omit = homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* homeassistant/components/keymitt_ble/__init__.py + homeassistant/components/keymitt_ble/coordinator.py homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/switch.py - homeassistant/components/keymitt_ble/coordinator.py homeassistant/components/kitchen_sink/weather.py homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py @@ -733,6 +741,7 @@ omit = homeassistant/components/lutron/binary_sensor.py homeassistant/components/lutron/cover.py homeassistant/components/lutron/entity.py + homeassistant/components/lutron/event.py homeassistant/components/lutron/fan.py homeassistant/components/lutron/light.py homeassistant/components/lutron/switch.py @@ -836,8 +845,15 @@ omit = homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py - homeassistant/components/mystrom/switch.py homeassistant/components/mystrom/sensor.py + homeassistant/components/mystrom/switch.py + homeassistant/components/myuplink/__init__.py + homeassistant/components/myuplink/api.py + homeassistant/components/myuplink/application_credentials.py + homeassistant/components/myuplink/coordinator.py + homeassistant/components/myuplink/entity.py + homeassistant/components/myuplink/helpers.py + homeassistant/components/myuplink/sensor.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py @@ -845,13 +861,13 @@ omit = homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py + homeassistant/components/neato/button.py homeassistant/components/neato/camera.py homeassistant/components/neato/entity.py homeassistant/components/neato/hub.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py - homeassistant/components/neato/button.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py @@ -961,15 +977,16 @@ omit = homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/__init__.py + homeassistant/components/opnsense/device_tracker.py homeassistant/components/opower/__init__.py homeassistant/components/opower/coordinator.py homeassistant/components/opower/sensor.py - homeassistant/components/opnsense/device_tracker.py homeassistant/components/opple/light.py homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osoenergy/__init__.py homeassistant/components/osoenergy/const.py + homeassistant/components/osoenergy/sensor.py homeassistant/components/osoenergy/water_heater.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py @@ -1091,17 +1108,6 @@ omit = homeassistant/components/rainmachine/switch.py homeassistant/components/rainmachine/update.py homeassistant/components/rainmachine/util.py - homeassistant/components/renson/__init__.py - homeassistant/components/renson/const.py - homeassistant/components/renson/coordinator.py - homeassistant/components/renson/entity.py - homeassistant/components/renson/sensor.py - homeassistant/components/renson/button.py - homeassistant/components/renson/fan.py - homeassistant/components/renson/switch.py - homeassistant/components/renson/binary_sensor.py - homeassistant/components/renson/number.py - homeassistant/components/renson/time.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/sensor.py homeassistant/components/recorder/repack.py @@ -1116,6 +1122,17 @@ omit = homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote_rpi_gpio/* + homeassistant/components/renson/__init__.py + homeassistant/components/renson/binary_sensor.py + homeassistant/components/renson/button.py + homeassistant/components/renson/const.py + homeassistant/components/renson/coordinator.py + homeassistant/components/renson/entity.py + homeassistant/components/renson/fan.py + homeassistant/components/renson/number.py + homeassistant/components/renson/sensor.py + homeassistant/components/renson/switch.py + homeassistant/components/renson/time.py homeassistant/components/reolink/binary_sensor.py homeassistant/components/reolink/button.py homeassistant/components/reolink/camera.py @@ -1141,8 +1158,10 @@ omit = homeassistant/components/roborock/coordinator.py homeassistant/components/rocketchat/notify.py homeassistant/components/romy/__init__.py + homeassistant/components/romy/binary_sensor.py homeassistant/components/romy/coordinator.py homeassistant/components/romy/entity.py + homeassistant/components/romy/sensor.py homeassistant/components/romy/vacuum.py homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py @@ -1159,23 +1178,24 @@ omit = homeassistant/components/route53/* homeassistant/components/rpi_camera/* homeassistant/components/rtorrent/sensor.py + homeassistant/components/russound_rio/media_player.py + homeassistant/components/russound_rnet/media_player.py homeassistant/components/ruuvi_gateway/__init__.py homeassistant/components/ruuvi_gateway/bluetooth.py homeassistant/components/ruuvi_gateway/coordinator.py - homeassistant/components/russound_rio/media_player.py - homeassistant/components/russound_rnet/media_player.py homeassistant/components/rympro/__init__.py homeassistant/components/rympro/coordinator.py homeassistant/components/rympro/sensor.py homeassistant/components/sabnzbd/__init__.py + homeassistant/components/sabnzbd/coordinator.py homeassistant/components/sabnzbd/sensor.py homeassistant/components/saj/sensor.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py - homeassistant/components/screenlogic/coordinator.py homeassistant/components/screenlogic/const.py + homeassistant/components/screenlogic/coordinator.py homeassistant/components/screenlogic/entity.py homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py @@ -1250,8 +1270,8 @@ omit = homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/__init__.py - homeassistant/components/solarlog/sensor.py homeassistant/components/solarlog/coordinator.py + homeassistant/components/solarlog/sensor.py homeassistant/components/solax/__init__.py homeassistant/components/solax/sensor.py homeassistant/components/soma/__init__.py @@ -1288,14 +1308,6 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/starlink/__init__.py - homeassistant/components/starlink/binary_sensor.py - homeassistant/components/starlink/button.py - homeassistant/components/starlink/coordinator.py - 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 @@ -1306,6 +1318,14 @@ omit = homeassistant/components/starline/sensor.py homeassistant/components/starline/switch.py homeassistant/components/starlingbank/sensor.py + homeassistant/components/starlink/__init__.py + homeassistant/components/starlink/binary_sensor.py + homeassistant/components/starlink/button.py + homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/device_tracker.py + homeassistant/components/starlink/sensor.py + homeassistant/components/starlink/switch.py + homeassistant/components/starlink/time.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* homeassistant/components/stookalert/__init__.py @@ -1349,9 +1369,9 @@ omit = homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/humidifier.py homeassistant/components/switchbot/light.py + homeassistant/components/switchbot/lock.py homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/switch.py - homeassistant/components/switchbot/lock.py homeassistant/components/switchbot_cloud/climate.py homeassistant/components/switchbot_cloud/coordinator.py homeassistant/components/switchbot_cloud/entity.py @@ -1391,11 +1411,6 @@ omit = homeassistant/components/tado/water_heater.py homeassistant/components/tami4/button.py homeassistant/components/tank_utility/sensor.py - homeassistant/components/tankerkoenig/__init__.py - homeassistant/components/tankerkoenig/binary_sensor.py - homeassistant/components/tankerkoenig/coordinator.py - homeassistant/components/tankerkoenig/entity.py - homeassistant/components/tankerkoenig/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/__init__.py homeassistant/components/tautulli/coordinator.py @@ -1464,6 +1479,7 @@ omit = homeassistant/components/traccar_server/device_tracker.py homeassistant/components/traccar_server/entity.py homeassistant/components/traccar_server/helpers.py + homeassistant/components/traccar_server/sensor.py homeassistant/components/tractive/__init__.py homeassistant/components/tractive/binary_sensor.py homeassistant/components/tractive/device_tracker.py @@ -1511,9 +1527,9 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py homeassistant/components/ukraine_alarm/binary_sensor.py - homeassistant/components/unifiled/* homeassistant/components/unifi_direct/__init__.py homeassistant/components/unifi_direct/device_tracker.py + homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py homeassistant/components/upb/light.py homeassistant/components/upc_connect/* @@ -1523,7 +1539,6 @@ omit = homeassistant/components/upnp/__init__.py homeassistant/components/upnp/device.py homeassistant/components/upnp/sensor.py - homeassistant/components/vasttrafik/sensor.py homeassistant/components/v2c/__init__.py homeassistant/components/v2c/binary_sensor.py homeassistant/components/v2c/coordinator.py @@ -1531,6 +1546,7 @@ omit = homeassistant/components/v2c/number.py homeassistant/components/v2c/sensor.py homeassistant/components/v2c/switch.py + homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py homeassistant/components/velbus/binary_sensor.py homeassistant/components/velbus/button.py @@ -1538,8 +1554,8 @@ omit = homeassistant/components/velbus/cover.py homeassistant/components/velbus/entity.py homeassistant/components/velbus/light.py - homeassistant/components/velbus/sensor.py homeassistant/components/velbus/select.py + homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/__init__.py homeassistant/components/velux/cover.py @@ -1705,12 +1721,12 @@ omit = homeassistant/components/zeversolar/coordinator.py homeassistant/components/zeversolar/entity.py homeassistant/components/zeversolar/sensor.py - homeassistant/components/zha/websocket_api.py homeassistant/components/zha/core/cluster_handlers/* homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py homeassistant/components/zha/light.py + homeassistant/components/zha/websocket_api.py homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* @@ -1727,15 +1743,6 @@ omit = homeassistant/components/zwave_me/sensor.py homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/switch.py - homeassistant/components/electrasmart/climate.py - homeassistant/components/electrasmart/__init__.py - homeassistant/components/myuplink/__init__.py - homeassistant/components/myuplink/api.py - homeassistant/components/myuplink/application_credentials.py - homeassistant/components/myuplink/coordinator.py - homeassistant/components/myuplink/entity.py - homeassistant/components/myuplink/helpers.py - homeassistant/components/myuplink/sensor.py [report] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83aa88140cc..2bdb6f99aad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,10 @@ "dockerFile": "../Dockerfile.dev", "postCreateCommand": "script/setup", "postStartCommand": "script/bootstrap", - "containerEnv": { "DEVCONTAINER": "1" }, + "containerEnv": { + "DEVCONTAINER": "1", + "PYTHONASYNCIODEBUG": "1" + }, // Port 5683 udp is used by Shelly integration "appPort": ["8123:8123", "5683:5683/udp"], "runArgs": ["-e", "GIT_EDITOR=code --wait"], diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 217093793d1..bc70eafd3f4 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -27,7 +27,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 with: fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: translations path: translations.tar.gz @@ -90,7 +90,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -174,17 +174,8 @@ jobs: sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt - - name: Adjustments for 64-bit - if: matrix.arch == 'amd64' || matrix.arch == 'aarch64' - run: | - # Some speedups are only available on 64-bit, and since - # we build 32bit images on 64bit hosts, we only enable - # the speed ups on 64bit since the wheels for 32bit - # are not available. - sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt - - name: Download translations - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: translations @@ -251,7 +242,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set build additional args run: | @@ -288,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -329,7 +320,7 @@ jobs: registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Install Cosign uses: sigstore/cosign-installer@v3.4.0 @@ -459,7 +450,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 @@ -467,7 +458,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Download translations - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: translations diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a7e38f0110..115c1a932ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,10 +33,10 @@ on: type: boolean env: - CACHE_VERSION: 5 + CACHE_VERSION: 8 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 - HA_SHORT_VERSION: "2024.4" + HA_SHORT_VERSION: "2024.5" DEFAULT_PYTHON: "3.12" ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version @@ -89,14 +89,16 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- echo "key=venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT + hashFiles('homeassistant/package_constraints.txt') }}-${{ + hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT - name: Generate partial pre-commit restore key id: generate_pre-commit_cache_key run: >- @@ -222,7 +224,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -268,7 +270,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -308,7 +310,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -347,7 +349,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -441,7 +443,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -450,8 +452,10 @@ jobs: check-latest: true - name: Generate partial uv restore key id: generate-uv-key - run: >- - echo "key=uv-${{ env.UV_CACHE_VERSION }}-${{ + run: | + uv_version=$(cat requirements_test.txt | grep uv | cut -d '=' -f 3) + echo "version=${uv_version}" >> $GITHUB_OUTPUT + echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{ env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv @@ -471,10 +475,13 @@ jobs: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-uv-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ + env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{ + env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update sudo apt-get -y install \ bluez \ @@ -484,6 +491,7 @@ jobs: libavfilter-dev \ libavformat-dev \ libavutil-dev \ + libgammu-dev \ libswresample-dev \ libswscale-dev \ libudev-dev @@ -495,7 +503,9 @@ jobs: python --version 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.txt + python -m script.gen_requirements_all ci + uv pip install -r requirements_all_pytest.txt uv pip install -r requirements_test.txt uv pip install -e . --config-settings editable_mode=compat @@ -510,7 +520,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -542,7 +552,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -575,7 +585,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -619,7 +629,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -670,14 +680,63 @@ jobs: python --version mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} - pytest: + prepare-pytest-full: runs-on: ubuntu-22.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' - && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) + && needs.info.outputs.test_full_suite == 'true' + needs: + - info + - base + name: Split tests for full run + steps: + - name: Install additional OS dependencies + run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg \ + libgammu-dev + - name: Check out code from GitHub + uses: actions/checkout@v4.1.3 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + 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.2 + with: + path: venv + fail-on-cache-miss: true + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} + - name: Run split_tests.py + run: | + . venv/bin/activate + python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests + - name: Upload pytest_buckets + uses: actions/upload-artifact@v4.3.3 + with: + name: pytest_buckets + path: pytest_buckets.txt + overwrite: true + + pytest-full: + runs-on: ubuntu-22.04 + if: | + (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') + && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' + && needs.info.outputs.test_full_suite == 'true' needs: - info - base @@ -686,6 +745,7 @@ jobs: - lint-other - lint-ruff - mypy + - prepare-pytest-full strategy: fail-fast: false matrix: @@ -696,12 +756,14 @@ jobs: steps: - name: Install additional OS dependencies run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update sudo apt-get -y install \ bluez \ - ffmpeg + ffmpeg \ + libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -722,12 +784,15 @@ jobs: - name: Register pytest slow test problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" + - name: Download pytest_buckets + uses: actions/download-artifact@v4.1.6 + with: + name: pytest_buckets - name: Compile English translations run: | . venv/bin/activate python3 -m script.translations develop --all - - name: Run pytest (fully) - if: needs.info.outputs.test_full_suite == 'true' + - name: Run pytest timeout-minutes: 60 id: pytest-full env: @@ -748,62 +813,27 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count ${{ needs.info.outputs.test_group_count }} \ - --test-group=${{ matrix.group }} \ ${cov_params[@]} \ -o console_output_style=count \ -p no:sugar \ - tests \ - 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - - name: Run pytest (partially) - if: needs.info.outputs.test_full_suite == 'false' - timeout-minutes: 10 - id: pytest-partial - shell: bash - env: - PYTHONDONTWRITEBYTECODE: 1 - run: | - . venv/bin/activate - python --version - set -o pipefail - - if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then - echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" - exit 1 - fi - - cov_params=() - if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then - cov_params+=(--cov="homeassistant.components.${{ matrix.group }}") - cov_params+=(--cov-report=xml) - cov_params+=(--cov-report=term-missing) - fi - - python3 -b -X dev -m pytest \ - -qq \ - --timeout=9 \ - -n auto \ - ${cov_params[@]} \ - -o console_output_style=count \ - --durations=0 \ - --durations-min=1 \ - -p no:sugar \ - tests/components/${{ matrix.group }} \ + $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \ 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@v4.3.1 + if: success() || failure() && steps.pytest-full.conclusion == 'failure' + uses: actions/upload-artifact@v4.3.3 with: 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 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml overwrite: true + - name: Remove pytest_buckets + run: rm pytest_buckets.txt - name: Check dirty run: | ./script/check_dirty @@ -842,13 +872,14 @@ jobs: steps: - name: Install additional OS dependencies run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -912,7 +943,7 @@ 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@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -920,7 +951,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -964,13 +995,14 @@ jobs: steps: - name: Install additional OS dependencies run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -1035,7 +1067,7 @@ 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@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1043,7 +1075,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1053,39 +1085,160 @@ jobs: run: | ./script/check_dirty - coverage: - name: Upload test coverage to Codecov + coverage-full: + name: Upload test coverage to Codecov (full suite) if: needs.info.outputs.skip_coverage != 'true' runs-on: ubuntu-22.04 needs: - info - - pytest + - pytest-full + - pytest-postgres + - pytest-mariadb timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: pattern: coverage-* - - name: Upload coverage to Codecov (full coverage) + - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v2.1.0 + uses: codecov/codecov-action@v4.3.0 with: - action: codecov/codecov-action@v3.1.3 - with: | - fail_ci_if_error: true - flags: full-suite - token: ${{ env.CODECOV_TOKEN }} - attempt_limit: 5 - attempt_delay: 30000 - - name: Upload coverage to Codecov (partial coverage) + fail_ci_if_error: true + flags: full-suite + token: ${{ secrets.CODECOV_TOKEN }} + + pytest-partial: + runs-on: ubuntu-22.04 + if: | + (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') + && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' + && needs.info.outputs.tests_glob + && needs.info.outputs.test_full_suite == 'false' + needs: + - info + - base + - gen-requirements-all + - hassfest + - lint-other + - lint-ruff + - mypy + strategy: + fail-fast: false + matrix: + group: ${{ fromJson(needs.info.outputs.test_groups) }} + python-version: ${{ fromJson(needs.info.outputs.python_versions) }} + name: >- + Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) + steps: + - name: Install additional OS dependencies + run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg \ + libgammu-dev + - name: Check out code from GitHub + uses: actions/checkout@v4.1.3 + - name: Set up Python ${{ matrix.python-version }} + id: python + 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.2 + with: + path: venv + fail-on-cache-miss: true + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Register pytest slow test problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" + - name: Compile English translations + run: | + . venv/bin/activate + python3 -m script.translations develop --all + - name: Run pytest + timeout-minutes: 10 + id: pytest-partial + shell: bash + env: + PYTHONDONTWRITEBYTECODE: 1 + run: | + . venv/bin/activate + python --version + set -o pipefail + + if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then + echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" + exit 1 + fi + + cov_params=() + if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then + cov_params+=(--cov="homeassistant.components.${{ matrix.group }}") + cov_params+=(--cov-report=xml) + cov_params+=(--cov-report=term-missing) + fi + + python3 -b -X dev -m pytest \ + -qq \ + --timeout=9 \ + -n auto \ + ${cov_params[@]} \ + -o console_output_style=count \ + --durations=0 \ + --durations-min=1 \ + -p no:sugar \ + tests/components/${{ matrix.group }} \ + 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt + - name: Upload pytest output + if: success() || failure() && steps.pytest-partial.conclusion == 'failure' + uses: actions/upload-artifact@v4.3.3 + with: + 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.3 + with: + name: coverage-${{ matrix.python-version }}-${{ matrix.group }} + path: coverage.xml + overwrite: true + - name: Check dirty + run: | + ./script/check_dirty + + coverage-partial: + name: Upload test coverage to Codecov (partial suite) + if: needs.info.outputs.skip_coverage != 'true' + runs-on: ubuntu-22.04 + needs: + - info + - pytest-partial + timeout-minutes: 10 + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.3 + - name: Download all coverage artifacts + uses: actions/download-artifact@v4.1.6 + with: + pattern: coverage-* + - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v2.1.0 + uses: codecov/codecov-action@v4.3.0 with: - action: codecov/codecov-action@v3.1.3 - with: | - fail_ci_if_error: true - token: ${{ env.CODECOV_TOKEN }} - attempt_limit: 5 - attempt_delay: 30000 + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index aa9822e0131..d1393c97462 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.2 + uses: actions/checkout@v4.1.3 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.24.9 + uses: github/codeql-action/init@v3.25.2 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.24.9 + uses: github/codeql-action/analyze@v3.25.2 with: category: "/language:python" diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index e61eef36f0b..3f0559de541 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9f127acb57d..2627ac70795 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -14,6 +14,10 @@ on: - "homeassistant/package_constraints.txt" - "requirements_all.txt" - "requirements.txt" + - "script/gen_requirements_all.py" + +env: + DEFAULT_PYTHON: "3.12" concurrency: group: ${{ github.workflow }}-${{ github.ref_name}} @@ -28,7 +32,22 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + + - name: Create Python virtual environment + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install "$(grep '^uv' < requirements_test.txt)" + uv pip install -r requirements.txt - name: Get information id: info @@ -63,19 +82,30 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: env_file path: ./.env_file overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: requirements_diff path: ./requirements_diff.txt overwrite: true + - name: Generate requirements + run: | + . venv/bin/activate + python -m script.gen_requirements_all ci + + - name: Upload requirements_all_wheels + uses: actions/upload-artifact@v4.3.3 + with: + name: requirements_all_wheels + path: ./requirements_all_wheels_*.txt + core: name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2) if: github.repository_owner == 'home-assistant' @@ -88,15 +118,15 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download env_file - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: requirements_diff @@ -126,57 +156,30 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download env_file - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: requirements_diff - - name: (Un)comment packages - run: | - requirement_files="requirements_all.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} - sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} - sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# pycups|pycups|g" ${requirement_file} - sed -i "s|# homekit|homekit|g" ${requirement_file} - sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file} - sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - - # Some packages are not buildable on armhf anymore - if [ "${{ matrix.arch }}" = "armhf" ]; then - - # Pandas has issues building on armhf, it is expected they - # will drop the platform in the near future (they consider it - # "flimsy" on 386). The following packages depend on pandas, - # so we comment them out. - sed -i "s|env-canada|# env-canada|g" ${requirement_file} - sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file} - sed -i "s|pyezviz|# pyezviz|g" ${requirement_file} - sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file} - fi - - # Some speedups are only for 64-bit - if [ "${{ matrix.arch }}" = "amd64" ] || [ "${{ matrix.arch }}" = "aarch64" ]; then - sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" ${requirement_file} - fi - - done + - name: Download requirements_all_wheels + uses: actions/download-artifact@v4.1.6 + with: + name: requirements_all_wheels - name: Split requirements all run: | - # We split requirements all into two different files. + # We split requirements all into multiple files. # This is to prevent the build from running out of memory when # resolving packages on 32-bits systems (like armhf, armv7). - split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all.txt requirements_all.txt + split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt - name: Create requirements for cython<3 run: | diff --git a/.gitignore b/.gitignore index 8a4154e4769..206595f06c9 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ tmp_cache # python-language-server / Rope .ropeproject + +# Will be created from script/split_tests.py +pytest_buckets.txt \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef4cdd98efb..ceb8ee7f9c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.1 hooks: - id: ruff args: - --fix - id: ruff-format - files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ + files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$ - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: @@ -15,7 +15,7 @@ repos: - --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] + exclude_types: [csv, json, html] exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 @@ -63,7 +63,7 @@ repos: language: script types: [python] require_serial: true - files: ^(homeassistant|pylint)/.+\.py$ + files: ^(homeassistant|pylint)/.+\.(py|pyi)$ - id: pylint name: pylint entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y @@ -83,7 +83,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$ + files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$ - id: hassfest-metadata name: hassfest-metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata diff --git a/.strict-typing b/.strict-typing index fb621d3e53a..5985938885f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -66,6 +66,7 @@ homeassistant.components.alpha_vantage.* homeassistant.components.amazon_polly.* homeassistant.components.amberelectric.* homeassistant.components.ambiclimate.* +homeassistant.components.ambient_network.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* @@ -166,10 +167,12 @@ homeassistant.components.electric_kiwi.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* +homeassistant.components.energenie_power_sockets.* homeassistant.components.energy.* homeassistant.components.energyzero.* homeassistant.components.enigma2.* homeassistant.components.enphase_envoy.* +homeassistant.components.eq3btsmart.* homeassistant.components.esphome.* homeassistant.components.event.* homeassistant.components.evil_genius_labs.* @@ -361,6 +364,7 @@ homeassistant.components.rest_command.* homeassistant.components.rfxtrx.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* +homeassistant.components.ring.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.romy.* diff --git a/CODEOWNERS b/CODEOWNERS index c6cee80ea80..c8a391fd7dc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,13 +5,30 @@ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Home Assistant Core -setup.cfg @home-assistant/core +.core_files.yaml @home-assistant/core +.git-blame-ignore-revs @home-assistant/core +.gitattributes @home-assistant/core +.gitignore @home-assistant/core +.hadolint.yaml @home-assistant/core +.pre-commit-config.yaml @home-assistant/core +.prettierignore @home-assistant/core +.yamllint @home-assistant/core pyproject.toml @home-assistant/core +requirements_test.txt @home-assistant/core +/.devcontainer/ @home-assistant/core +/.github/ @home-assistant/core +/.vscode/ @home-assistant/core /homeassistant/*.py @home-assistant/core +/homeassistant/auth/ @home-assistant/core +/homeassistant/backports/ @home-assistant/core /homeassistant/helpers/ @home-assistant/core +/homeassistant/scripts/ @home-assistant/core /homeassistant/util/ @home-assistant/core +/pylint/ @home-assistant/core +/script/ @home-assistant/core # Home Assistant Supervisor +.dockerignore @home-assistant/supervisor build.json @home-assistant/supervisor /machine/ @home-assistant/supervisor /rootfs/ @home-assistant/supervisor @@ -73,6 +90,8 @@ build.json @home-assistant/supervisor /tests/components/amberelectric/ @madpilot /homeassistant/components/ambiclimate/ @danielhiversen /tests/components/ambiclimate/ @danielhiversen +/homeassistant/components/ambient_network/ @thomaskistler +/tests/components/ambient_network/ @thomaskistler /homeassistant/components/ambient_station/ @bachya /tests/components/ambient_station/ @bachya /homeassistant/components/amcrest/ @flacjacket @@ -113,6 +132,8 @@ build.json @home-assistant/supervisor /homeassistant/components/arcam_fmj/ @elupus /tests/components/arcam_fmj/ @elupus /homeassistant/components/arris_tg2492lg/ @vanbalken +/homeassistant/components/arve/ @ikalnyi +/tests/components/arve/ @ikalnyi /homeassistant/components/aseko_pool_live/ @milanmeu /tests/components/aseko_pool_live/ @milanmeu /homeassistant/components/assist_pipeline/ @balloob @synesthesiam @@ -299,8 +320,8 @@ build.json @home-assistant/supervisor /tests/components/discovergy/ @jpbede /homeassistant/components/dlink/ @tkdrob /tests/components/dlink/ @tkdrob -/homeassistant/components/dlna_dmr/ @StevenLooman @chishm -/tests/components/dlna_dmr/ @StevenLooman @chishm +/homeassistant/components/dlna_dmr/ @chishm +/tests/components/dlna_dmr/ @chishm /homeassistant/components/dlna_dms/ @chishm /tests/components/dlna_dms/ @chishm /homeassistant/components/dnsip/ @gjohansson-ST @@ -361,11 +382,14 @@ build.json @home-assistant/supervisor /tests/components/emulated_hue/ @bdraco @Tho85 /homeassistant/components/emulated_kasa/ @kbickar /tests/components/emulated_kasa/ @kbickar +/homeassistant/components/energenie_power_sockets/ @gnumpi +/tests/components/energenie_power_sockets/ @gnumpi /homeassistant/components/energy/ @home-assistant/core /tests/components/energy/ @home-assistant/core /homeassistant/components/energyzero/ @klaasnicolaas /tests/components/energyzero/ @klaasnicolaas /homeassistant/components/enigma2/ @autinerd +/tests/components/enigma2/ @autinerd /homeassistant/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac @@ -374,11 +398,14 @@ build.json @home-assistant/supervisor /homeassistant/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/ephember/ @ttroy50 +/homeassistant/components/epic_games_store/ @hacf-fr @Quentame +/tests/components/epic_games_store/ @hacf-fr @Quentame /homeassistant/components/epion/ @lhgravendeel /tests/components/epion/ @lhgravendeel /homeassistant/components/epson/ @pszafer /tests/components/epson/ @pszafer -/homeassistant/components/epsonworkforce/ @ThaStealth +/homeassistant/components/eq3btsmart/ @eulemitkeule @dbuezas +/tests/components/eq3btsmart/ @eulemitkeule @dbuezas /homeassistant/components/escea/ @lazdavila /tests/components/escea/ @lazdavila /homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco @@ -435,8 +462,8 @@ build.json @home-assistant/supervisor /homeassistant/components/forked_daapd/ @uvjustin /tests/components/forked_daapd/ @uvjustin /homeassistant/components/fortios/ @kimfrellsen -/homeassistant/components/foscam/ @skgsergio @krmarien -/tests/components/foscam/ @skgsergio @krmarien +/homeassistant/components/foscam/ @krmarien +/tests/components/foscam/ @krmarien /homeassistant/components/freebox/ @hacf-fr @Quentame /tests/components/freebox/ @hacf-fr @Quentame /homeassistant/components/freedompro/ @stefano055415 @@ -574,6 +601,8 @@ build.json @home-assistant/supervisor /tests/components/homekit_controller/ @Jc2k @bdraco /homeassistant/components/homematic/ @pvizeli /tests/components/homematic/ @pvizeli +/homeassistant/components/homematicip_cloud/ @hahn-th +/tests/components/homematicip_cloud/ @hahn-th /homeassistant/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL /homeassistant/components/honeywell/ @rdfurman @mkmer @@ -661,8 +690,8 @@ build.json @home-assistant/supervisor /homeassistant/components/iqvia/ @bachya /tests/components/iqvia/ @bachya /homeassistant/components/irish_rail_transport/ @ttroy50 -/homeassistant/components/islamic_prayer_times/ @engrbm87 -/tests/components/islamic_prayer_times/ @engrbm87 +/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair +/tests/components/islamic_prayer_times/ @engrbm87 @cpfair /homeassistant/components/iss/ @DurgNomis-drol /tests/components/iss/ @DurgNomis-drol /homeassistant/components/isy994/ @bdraco @shbatm @@ -731,7 +760,8 @@ build.json @home-assistant/supervisor /tests/components/leaone/ @bdraco /homeassistant/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco -/homeassistant/components/lg_netcast/ @Drafteed +/homeassistant/components/lg_netcast/ @Drafteed @splinter98 +/tests/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob /homeassistant/components/light/ @home-assistant/core @@ -847,8 +877,8 @@ build.json @home-assistant/supervisor /tests/components/motioneye/ @dermotduffy /homeassistant/components/motionmount/ @RJPoelstra /tests/components/motionmount/ @RJPoelstra -/homeassistant/components/mqtt/ @emontnemery @jbouwh -/tests/components/mqtt/ @emontnemery @jbouwh +/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco +/tests/components/mqtt/ @emontnemery @jbouwh @bdraco /homeassistant/components/msteams/ @peroyvind /homeassistant/components/mullvad/ @meichthys /tests/components/mullvad/ @meichthys @@ -1001,8 +1031,8 @@ build.json @home-assistant/supervisor /tests/components/persistent_notification/ @home-assistant/core /homeassistant/components/philips_js/ @elupus /tests/components/philips_js/ @elupus -/homeassistant/components/pi_hole/ @johnluetke @shenxn -/tests/components/pi_hole/ @johnluetke @shenxn +/homeassistant/components/pi_hole/ @shenxn +/tests/components/pi_hole/ @shenxn /homeassistant/components/picnic/ @corneyl /tests/components/picnic/ @corneyl /homeassistant/components/pilight/ @trekky12 @@ -1056,8 +1086,8 @@ build.json @home-assistant/supervisor /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39 /tests/components/qbittorrent/ @geoffreylagaisse @finder39 -/homeassistant/components/qingping/ @bdraco @skgsergio -/tests/components/qingping/ @bdraco @skgsergio +/homeassistant/components/qingping/ @bdraco +/tests/components/qingping/ @bdraco /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap/ @disforw @@ -1156,11 +1186,13 @@ build.json @home-assistant/supervisor /tests/components/ruuvitag_ble/ @akx /homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc /tests/components/rympro/ @OnFreund @elad-bar @maorcc -/homeassistant/components/sabnzbd/ @shaiu -/tests/components/sabnzbd/ @shaiu +/homeassistant/components/sabnzbd/ @shaiu @jpbede +/tests/components/sabnzbd/ @shaiu @jpbede /homeassistant/components/saj/ @fredericvl /homeassistant/components/samsungtv/ @chemelli74 @epenet /tests/components/samsungtv/ @chemelli74 @epenet +/homeassistant/components/sanix/ @tomaszsluszniak +/tests/components/sanix/ @tomaszsluszniak /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schedule/ @home-assistant/core @@ -1247,14 +1279,15 @@ build.json @home-assistant/supervisor /homeassistant/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST /homeassistant/components/sms/ @ocalvo +/tests/components/sms/ @ocalvo /homeassistant/components/snapcast/ @luar123 /tests/components/snapcast/ @luar123 /homeassistant/components/snmp/ @nmaggioni /tests/components/snmp/ @nmaggioni /homeassistant/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst -/homeassistant/components/solaredge/ @frenck -/tests/components/solaredge/ @frenck +/homeassistant/components/solaredge/ @frenck @bdraco +/tests/components/solaredge/ @frenck @bdraco /homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solarlog/ @Ernst79 /tests/components/solarlog/ @Ernst79 @@ -1266,8 +1299,8 @@ build.json @home-assistant/supervisor /tests/components/sonarr/ @ctalkington /homeassistant/components/songpal/ @rytilahti @shenxn /tests/components/songpal/ @rytilahti @shenxn -/homeassistant/components/sonos/ @jjlawren -/tests/components/sonos/ @jjlawren +/homeassistant/components/sonos/ @jjlawren @peterager +/tests/components/sonos/ @jjlawren @peterager /homeassistant/components/soundtouch/ @kroimon /tests/components/soundtouch/ @kroimon /homeassistant/components/spaceapi/ @fabaff @@ -1551,8 +1584,8 @@ build.json @home-assistant/supervisor /tests/components/wiz/ @sbidy /homeassistant/components/wled/ @frenck /tests/components/wled/ @frenck -/homeassistant/components/wolflink/ @adamkrol93 -/tests/components/wolflink/ @adamkrol93 +/homeassistant/components/wolflink/ @adamkrol93 @mtielen +/tests/components/wolflink/ @adamkrol93 @mtielen /homeassistant/components/workday/ @fabaff @gjohansson-ST /tests/components/workday/ @fabaff @gjohansson-ST /homeassistant/components/worldclock/ @fabaff diff --git a/Dockerfile b/Dockerfile index 2a27402be6c..c916a3d2f3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV \ ARG QEMU_CPU # Install uv -RUN pip3 install uv==0.1.24 +RUN pip3 install uv==0.1.35 WORKDIR /usr/src @@ -30,14 +30,10 @@ RUN \ 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 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" \ uv pip install \ --no-build \ -r homeassistant/requirements_all.txt; \ diff --git a/Dockerfile.dev b/Dockerfile.dev index e60456f7b1f..507cc9a7bb2 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -22,6 +22,7 @@ RUN \ libavcodec-dev \ libavdevice-dev \ libavutil-dev \ + libgammu-dev \ libswscale-dev \ libswresample-dev \ libavfilter-dev \ diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 4ed80c27bf0..0c0d535753c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -146,9 +146,7 @@ def get_arguments() -> argparse.Namespace: help="Skips validation of operating system", ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def check_threads() -> None: diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 969fcc3529e..2a9525181f6 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -28,6 +28,7 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .models import AuthFlowResult from .providers import AuthProvider, LoginFlow, auth_provider_from_config +from .session import SessionManager EVENT_USER_ADDED = "user_added" EVENT_USER_UPDATED = "user_updated" @@ -85,7 +86,7 @@ async def auth_manager_from_config( module_hash[module.id] = module manager = AuthManager(hass, store, provider_hash, module_hash) - manager.async_setup() + await manager.async_setup() return manager @@ -180,9 +181,9 @@ class AuthManager: self._remove_expired_job = HassJob( self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback ) + self.session = SessionManager(hass, self) - @callback - def async_setup(self) -> None: + async def async_setup(self) -> None: """Set up the auth manager.""" hass = self.hass hass.async_add_shutdown_job( @@ -191,6 +192,7 @@ class AuthManager: ) ) self._async_track_next_refresh_token_expiration() + await self.session.async_setup() @property def auth_providers(self) -> list[AuthProvider]: diff --git a/homeassistant/auth/jwt_wrapper.py b/homeassistant/auth/jwt_wrapper.py index 58f9260ff8f..3aa3ac63764 100644 --- a/homeassistant/auth/jwt_wrapper.py +++ b/homeassistant/auth/jwt_wrapper.py @@ -78,7 +78,7 @@ class _PyJWTWithVerify(PyJWT): key: str, algorithms: list[str], issuer: str | None = None, - leeway: int | float | timedelta = 0, + leeway: float | timedelta = 0, options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Verify a JWT's signature and claims.""" diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 2e5f5940544..7192f6345e1 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import datetime, timedelta +from functools import cached_property import secrets -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import Any, NamedTuple import uuid import attr @@ -18,12 +19,6 @@ from homeassistant.util import dt as dt_util from . import permissions as perm_mdl from .const import GROUP_ID_ADMIN -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - TOKEN_TYPE_NORMAL = "normal" TOKEN_TYPE_SYSTEM = "system" TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" @@ -91,11 +86,7 @@ class User: def invalidate_cache(self) -> None: """Invalidate permission and is_admin cache.""" for attr_to_invalidate in ("permissions", "is_admin"): - # try is must more efficient than suppress - try: # noqa: SIM105 - delattr(self, attr_to_invalidate) - except AttributeError: - pass + self.__dict__.pop(attr_to_invalidate, None) @attr.s(slots=True) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index c0574e9f0ea..9c2c7e500ca 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable -from typing import Any import voluptuous as vol @@ -64,7 +63,7 @@ class PolicyPermissions(AbstractPermissions): """Return a function that can test entity access.""" return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Equals check.""" return isinstance(other, PolicyPermissions) and other._policy == self._policy diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index 3146cd99787..9f2fb45f9f0 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Final +from typing import Any, Final from homeassistant.const import ( EVENT_COMPONENT_LOADED, @@ -21,10 +21,11 @@ from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED +from homeassistant.util.event_type import EventType # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. -SUBSCRIBE_ALLOWLIST: Final[set[str]] = { +SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = { EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/homeassistant/auth/session.py b/homeassistant/auth/session.py new file mode 100644 index 00000000000..88297b50d90 --- /dev/null +++ b/homeassistant/auth/session.py @@ -0,0 +1,205 @@ +"""Session auth module.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +import secrets +from typing import TYPE_CHECKING, Final, TypedDict + +from aiohttp.web import Request +from aiohttp_session import Session, get_session, new_session +from cryptography.fernet import Fernet + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.storage import Store +from homeassistant.util import dt as dt_util + +from .models import RefreshToken + +if TYPE_CHECKING: + from . import AuthManager + + +TEMP_TIMEOUT = timedelta(minutes=5) +TEMP_TIMEOUT_SECONDS = TEMP_TIMEOUT.total_seconds() + +SESSION_ID = "id" +STORAGE_VERSION = 1 +STORAGE_KEY = "auth.session" + + +class StrictConnectionTempSessionData: + """Data for accessing unauthorized resources for a short period of time.""" + + __slots__ = ("cancel_remove", "absolute_expiry") + + def __init__(self, cancel_remove: CALLBACK_TYPE) -> None: + """Initialize the temp session data.""" + self.cancel_remove: Final[CALLBACK_TYPE] = cancel_remove + self.absolute_expiry: Final[datetime] = dt_util.utcnow() + TEMP_TIMEOUT + + +class StoreData(TypedDict): + """Data to store.""" + + unauthorized_sessions: dict[str, str] + key: str + + +class SessionManager: + """Session manager.""" + + def __init__(self, hass: HomeAssistant, auth: AuthManager) -> None: + """Initialize the strict connection manager.""" + self._auth = auth + self._hass = hass + self._temp_sessions: dict[str, StrictConnectionTempSessionData] = {} + self._strict_connection_sessions: dict[str, str] = {} + self._store = Store[StoreData]( + hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True + ) + self._key: str | None = None + self._refresh_token_revoke_callbacks: dict[str, CALLBACK_TYPE] = {} + + @property + def key(self) -> str: + """Return the encryption key.""" + if self._key is None: + self._key = Fernet.generate_key().decode() + self._async_schedule_save() + return self._key + + async def async_validate_request_for_strict_connection_session( + self, + request: Request, + ) -> bool: + """Check if a request has a valid strict connection session.""" + session = await get_session(request) + if session.new or session.empty: + return False + result = self.async_validate_strict_connection_session(session) + if result is False: + session.invalidate() + return result + + @callback + def async_validate_strict_connection_session( + self, + session: Session, + ) -> bool: + """Validate a strict connection session.""" + if not (session_id := session.get(SESSION_ID)): + return False + + if token_id := self._strict_connection_sessions.get(session_id): + if self._auth.async_get_refresh_token(token_id): + return True + # refresh token is invalid, delete entry + self._strict_connection_sessions.pop(session_id) + self._async_schedule_save() + + if data := self._temp_sessions.get(session_id): + if dt_util.utcnow() <= data.absolute_expiry: + return True + # session expired, delete entry + self._temp_sessions.pop(session_id).cancel_remove() + + return False + + @callback + def _async_register_revoke_token_callback(self, refresh_token_id: str) -> None: + """Register a callback to revoke all sessions for a refresh token.""" + if refresh_token_id in self._refresh_token_revoke_callbacks: + return + + @callback + def async_invalidate_auth_sessions() -> None: + """Invalidate all sessions for a refresh token.""" + self._strict_connection_sessions = { + session_id: token_id + for session_id, token_id in self._strict_connection_sessions.items() + if token_id != refresh_token_id + } + self._async_schedule_save() + + self._refresh_token_revoke_callbacks[refresh_token_id] = ( + self._auth.async_register_revoke_token_callback( + refresh_token_id, async_invalidate_auth_sessions + ) + ) + + async def async_create_session( + self, + request: Request, + refresh_token: RefreshToken, + ) -> None: + """Create new session for given refresh token. + + Caller needs to make sure that the refresh token is valid. + By creating a session, we are implicitly revoking all other + sessions for the given refresh token as there is one refresh + token per device/user case. + """ + self._strict_connection_sessions = { + session_id: token_id + for session_id, token_id in self._strict_connection_sessions.items() + if token_id != refresh_token.id + } + + self._async_register_revoke_token_callback(refresh_token.id) + session_id = await self._async_create_new_session(request) + self._strict_connection_sessions[session_id] = refresh_token.id + self._async_schedule_save() + + async def async_create_temp_unauthorized_session(self, request: Request) -> None: + """Create a temporary unauthorized session.""" + session_id = await self._async_create_new_session( + request, max_age=int(TEMP_TIMEOUT_SECONDS) + ) + + @callback + def remove(_: datetime) -> None: + self._temp_sessions.pop(session_id, None) + + self._temp_sessions[session_id] = StrictConnectionTempSessionData( + async_call_later(self._hass, TEMP_TIMEOUT_SECONDS, remove) + ) + + async def _async_create_new_session( + self, + request: Request, + *, + max_age: int | None = None, + ) -> str: + session_id = secrets.token_hex(64) + + session = await new_session(request) + session[SESSION_ID] = session_id + if max_age is not None: + session.max_age = max_age + return session_id + + @callback + def _async_schedule_save(self, delay: float = 1) -> None: + """Save sessions.""" + self._store.async_delay_save(self._data_to_save, delay) + + @callback + def _data_to_save(self) -> StoreData: + """Return the data to store.""" + return StoreData( + unauthorized_sessions=self._strict_connection_sessions, + key=self.key, + ) + + async def async_setup(self) -> None: + """Set up session manager.""" + data = await self._store.async_load() + if data is None: + return + + self._key = data["key"] + self._strict_connection_sessions = data["unauthorized_sessions"] + for token_id in self._strict_connection_sessions.values(): + self._async_register_revoke_token_callback(token_id) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 3c09d8e7f57..8b823f47e22 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -9,8 +9,21 @@ import it. from __future__ import annotations -from enum import StrEnum +from enum import StrEnum as _StrEnum +from functools import partial -__all__ = [ - "StrEnum", -] +from homeassistant.helpers.deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + +# StrEnum deprecated as of 2024.5 use enum.StrEnum instead. +_DEPRECATED_StrEnum = DeprecatedAlias(_StrEnum, "enum.StrEnum", "2025.5") + +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index 8aab50eeb66..bad4236f9c8 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -1,79 +1,30 @@ -"""Functools backports from standard lib.""" +"""Functools backports from standard lib. -# This file contains parts of Python's module wrapper -# for the _functools C module -# to allow utilities written in Python to be added -# to the functools module. -# Written by Nick Coghlan , -# Raymond Hettinger , -# and Łukasz Langa . -# Copyright © 2001-2023 Python Software Foundation; All Rights Reserved +This file contained the backport of the cached_property implementation of Python 3.12. + +Since we have dropped support for Python 3.11, 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 collections.abc import Callable -from types import GenericAlias -from typing import Any, Generic, Self, TypeVar, overload +from functools import cached_property as _cached_property, partial -_T = TypeVar("_T") +from homeassistant.helpers.deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) +# cached_property deprecated as of 2024.5 use functools.cached_property instead. +_DEPRECATED_cached_property = DeprecatedAlias( + _cached_property, "functools.cached_property", "2025.5" +) -class cached_property(Generic[_T]): - """Backport of Python 3.12's cached_property. - - Includes https://github.com/python/cpython/pull/101890/files - """ - - def __init__(self, func: Callable[[Any], _T]) -> None: - """Initialize.""" - self.func: Callable[[Any], _T] = func - self.attrname: str | None = None - self.__doc__ = func.__doc__ - - def __set_name__(self, owner: type[Any], name: str) -> None: - """Set name.""" - if self.attrname is None: - self.attrname = name - elif name != self.attrname: - raise TypeError( - "Cannot assign the same cached_property to two different names " - f"({self.attrname!r} and {name!r})." - ) - - @overload - 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 | None, owner: type[Any] | None = None - ) -> _T | Self: - """Get.""" - if instance is None: - return self - if self.attrname is None: - raise TypeError( - "Cannot use cached_property instance without calling __set_name__ on it." - ) - try: - cache = instance.__dict__ - # not all objects have __dict__ (e.g. class defines slots) - except AttributeError: - msg = ( - f"No '__dict__' attribute on {type(instance).__name__!r} " - f"instance to cache {self.attrname!r} property." - ) - raise TypeError(msg) from None - val = self.func(instance) - try: - cache[self.attrname] = val - except TypeError: - msg = ( - f"The '__dict__' attribute on {type(instance).__name__!r} instance " - f"does not support item assignment for caching {self.attrname!r} property." - ) - raise TypeError(msg) from None - return val - - __class_getitem__ = classmethod(GenericAlias) # type: ignore[var-annotated] +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index bf805b5ef21..a2c187fc537 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -1,9 +1,36 @@ """Block blocking calls being done in asyncio.""" +from contextlib import suppress from http.client import HTTPConnection +import importlib +import sys import time +from typing import Any -from .util.async_ import protect_loop +from .helpers.frame import get_current_frame +from .util.loop import protect_loop + +_IN_TESTS = "unittest" in sys.modules + + +def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool: + # If the module is already imported, we can ignore it. + return bool((args := mapped_args.get("args")) and args[0] in sys.modules) + + +def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool: + # + # 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[0] is us + # frame[1] is check_loop + # frame[2] is protected_loop_func + # frame[3] is the offender + with suppress(ValueError): + return get_current_frame(4).f_code.co_filename.endswith("pydevd.py") + return False def enable() -> None: @@ -14,8 +41,20 @@ def enable() -> None: ) # Prevent sleeping in event loop. Non-strict since 2022.02 - time.sleep = protect_loop(time.sleep, strict=False) + time.sleep = protect_loop( + time.sleep, strict=False, check_allowed=_check_sleep_call_allowed + ) # Currently disabled. pytz doing I/O when getting timezone. # Prevent files being opened inside the event loop # builtins.open = protect_loop(builtins.open) + + if not _IN_TESTS: + # unittest uses `importlib.import_module` to do mocking + # so we cannot protect it if we are running tests + importlib.import_module = protect_loop( + importlib.import_module, + strict_core=False, + strict=False, + check_allowed=_check_import_call_allowed, + ) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5b805b6138e..fc5eedffc39 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -23,7 +23,14 @@ import cryptography.hazmat.backends.openssl.backend # noqa: F401 import voluptuous as vol import yarl -from . import config as conf_util, config_entries, core, loader, requirements +from . import ( + block_async_io, + config as conf_util, + config_entries, + core, + loader, + requirements, +) # 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 @@ -216,6 +223,7 @@ SETUP_ORDER = ( # If they do not exist they will not be loaded # PRELOAD_STORAGE = [ + "core.logger", "core.network", "http.auth", "image", @@ -245,6 +253,9 @@ async def async_setup_hass( runtime_config.log_no_color, ) + if runtime_config.debug or hass.loop.get_debug(): + hass.config.debug = True + hass.config.safe_mode = runtime_config.safe_mode hass.config.skip_pip = runtime_config.skip_pip hass.config.skip_pip_packages = runtime_config.skip_pip_packages @@ -260,6 +271,8 @@ async def async_setup_hass( _LOGGER.info("Config directory: %s", runtime_config.config_dir) loader.async_setup(hass) + block_async_io.enable() + config_dict = None basic_setup_success = False @@ -306,6 +319,7 @@ async def async_setup_hass( hass = core.HomeAssistant(old_config.config_dir) if old_logging: hass.data[DATA_LOGGING] = old_logging + hass.config.debug = old_config.debug hass.config.skip_pip = old_config.skip_pip hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.internal_url = old_config.internal_url @@ -564,7 +578,7 @@ def async_enable_logging( err_log_path, when="midnight", backupCount=log_rotate_days ) else: - err_handler = logging.handlers.RotatingFileHandler( + err_handler = _RotatingFileHandlerWithoutShouldRollOver( err_log_path, backupCount=1 ) @@ -588,6 +602,19 @@ def async_enable_logging( async_activate_log_queue_handler(hass) +class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): + """RotatingFileHandler that does not check if it should roll over on every log.""" + + def shouldRollover(self, record: logging.LogRecord) -> bool: + """Never roll over. + + The shouldRollover check is expensive because it has to stat + the log file for every log record. Since we do not set maxBytes + the result of this check is always False. + """ + return False + + async def async_mount_local_lib_path(config_dir: str) -> str: """Add local library to Python Path. @@ -704,7 +731,7 @@ async def async_setup_multi_components( # 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( + domain: hass.async_create_task_internal( async_setup_component(hass, domain, config), f"setup component {domain}", eager_start=True, diff --git a/homeassistant/brands/epson.json b/homeassistant/brands/epson.json deleted file mode 100644 index 80d5db942a2..00000000000 --- a/homeassistant/brands/epson.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "domain": "epson", - "name": "Epson", - "integrations": ["epson", "epsonworkforce"] -} diff --git a/homeassistant/brands/eq3.json b/homeassistant/brands/eq3.json index f5b1c8aeb87..4cdfbb015f4 100644 --- a/homeassistant/brands/eq3.json +++ b/homeassistant/brands/eq3.json @@ -1,5 +1,5 @@ { "domain": "eq3", "name": "eQ-3", - "integrations": ["maxcube"] + "integrations": ["maxcube", "eq3btsmart"] } diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 26e0c1331be..d52ef5e0ec6 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -2,14 +2,10 @@ from __future__ import annotations -from asyncio import timeout -from datetime import timedelta +from dataclasses import dataclass import logging -from typing import Any -from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError -from aiohttp import ClientSession -from aiohttp.client_exceptions import ClientConnectorError +from accuweather import AccuWeather from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.config_entries import ConfigEntry @@ -17,43 +13,70 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER +from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION +from .coordinator import ( + AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherObservationDataUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +@dataclass +class AccuWeatherData: + """Data for AccuWeather integration.""" + + coordinator_observation: AccuWeatherObservationDataUpdateCoordinator + coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AccuWeather as config entry.""" api_key: str = entry.data[CONF_API_KEY] name: str = entry.data[CONF_NAME] - assert entry.unique_id is not None - location_key = entry.unique_id - forecast: bool = entry.options.get(CONF_FORECAST, False) - _LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast) + location_key = entry.unique_id + + _LOGGER.debug("Using location_key: %s", location_key) websession = async_get_clientsession(hass) + accuweather = AccuWeather(api_key, websession, location_key=location_key) - coordinator = AccuWeatherDataUpdateCoordinator( - hass, websession, api_key, location_key, forecast, name + coordinator_observation = AccuWeatherObservationDataUpdateCoordinator( + hass, + accuweather, + name, + "observation", + UPDATE_INTERVAL_OBSERVATION, ) - await coordinator.async_config_entry_first_refresh() + + coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator( + hass, + accuweather, + name, + "daily forecast", + UPDATE_INTERVAL_DAILY_FORECAST, + ) + + await coordinator_observation.async_config_entry_first_refresh() + await coordinator_daily_forecast.async_config_entry_first_refresh() entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AccuWeatherData( + coordinator_observation=coordinator_observation, + coordinator_daily_forecast=coordinator_daily_forecast, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove ozone sensors from registry if they exist ent_reg = er.async_get(hass) for day in range(5): - unique_id = f"{coordinator.location_key}-ozone-{day}" + unique_id = f"{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) ent_reg.async_remove(entity_id) @@ -74,65 +97,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" await hass.config_entries.async_reload(entry.entry_id) - - -class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching AccuWeather data API.""" - - def __init__( - self, - hass: HomeAssistant, - session: ClientSession, - api_key: str, - location_key: str, - forecast: bool, - name: str, - ) -> None: - """Initialize.""" - self.location_key = location_key - self.forecast = forecast - self.accuweather = AccuWeather(api_key, session, location_key=location_key) - self.device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, location_key)}, - manufacturer=MANUFACTURER, - name=name, - # You don't need to provide specific details for the URL, - # so passing in _ characters is fine if the location key - # is correct - configuration_url=( - "http://accuweather.com/en/" - f"_/_/{location_key}/" - f"weather-forecast/{location_key}/" - ), - ) - - # Enabling the forecast download increases the number of requests per data - # update, we use 40 minutes for current condition only and 80 minutes for - # current condition and forecast as update interval to not exceed allowed number - # of requests. We have 50 requests allowed per day, so we use 36 and leave 14 as - # a reserve for restarting HA. - update_interval = timedelta(minutes=40) - if self.forecast: - update_interval *= 2 - _LOGGER.debug("Data will be update every %s", update_interval) - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - - async def _async_update_data(self) -> dict[str, Any]: - """Update data via library.""" - forecast: list[dict[str, Any]] = [] - try: - async with timeout(10): - current = await self.accuweather.async_get_current_conditions() - if self.forecast: - forecast = await self.accuweather.async_get_daily_forecast() - except ( - ApiError, - ClientConnectorError, - InvalidApiKeyError, - RequestsExceededError, - ) as error: - raise UpdateFailed(error) from error - _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) - return {**current, ATTR_FORECAST: forecast} diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index af7560d963a..71f7de89528 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -10,26 +10,12 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.schema_config_entry_flow import ( - SchemaFlowFormStep, - SchemaOptionsFlowHandler, -) -from .const import CONF_FORECAST, DOMAIN - -OPTIONS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_FORECAST, default=False): bool, - } -) -OPTIONS_FLOW = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA), -} +from .const import DOMAIN class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): @@ -87,9 +73,3 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: - """Options callback for AccuWeather.""" - return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 31925172d1c..1bbf5a36187 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta from typing import Final from homeassistant.components.weather import ( @@ -27,10 +28,8 @@ ATTR_CATEGORY: Final = "Category" ATTR_DIRECTION: Final = "Direction" ATTR_ENGLISH: Final = "English" ATTR_LEVEL: Final = "level" -ATTR_FORECAST: Final = "forecast" ATTR_SPEED: Final = "Speed" ATTR_VALUE: Final = "Value" -CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." MAX_FORECAST_DAYS: Final = 4 @@ -56,3 +55,5 @@ CONDITION_MAP = { for cond_ha, cond_codes in CONDITION_CLASSES.items() for cond_code in cond_codes } +UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40) +UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) diff --git a/homeassistant/components/accuweather/coordinator.py b/homeassistant/components/accuweather/coordinator.py new file mode 100644 index 00000000000..26fadd6806c --- /dev/null +++ b/homeassistant/components/accuweather/coordinator.py @@ -0,0 +1,124 @@ +"""The AccuWeather coordinator.""" + +from asyncio import timeout +from datetime import timedelta +import logging +from typing import TYPE_CHECKING, Any + +from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp.client_exceptions import ClientConnectorError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator, + TimestampDataUpdateCoordinator, + UpdateFailed, +) + +from .const import DOMAIN, MANUFACTURER + +EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError) + +_LOGGER = logging.getLogger(__name__) + + +class AccuWeatherObservationDataUpdateCoordinator( + DataUpdateCoordinator[dict[str, Any]] +): + """Class to manage fetching AccuWeather data API.""" + + def __init__( + self, + hass: HomeAssistant, + accuweather: AccuWeather, + name: str, + coordinator_type: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.accuweather = accuweather + self.location_key = accuweather.location_key + + if TYPE_CHECKING: + assert self.location_key is not None + + self.device_info = _get_device_info(self.location_key, name) + + super().__init__( + hass, + _LOGGER, + name=f"{name} ({coordinator_type})", + update_interval=update_interval, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + async with timeout(10): + result = await self.accuweather.async_get_current_conditions() + except EXCEPTIONS as error: + raise UpdateFailed(error) from error + + _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) + + return result + + +class AccuWeatherDailyForecastDataUpdateCoordinator( + TimestampDataUpdateCoordinator[list[dict[str, Any]]] +): + """Class to manage fetching AccuWeather data API.""" + + def __init__( + self, + hass: HomeAssistant, + accuweather: AccuWeather, + name: str, + coordinator_type: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.accuweather = accuweather + self.location_key = accuweather.location_key + + if TYPE_CHECKING: + assert self.location_key is not None + + self.device_info = _get_device_info(self.location_key, name) + + super().__init__( + hass, + _LOGGER, + name=f"{name} ({coordinator_type})", + update_interval=update_interval, + ) + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Update data via library.""" + try: + async with timeout(10): + result = await self.accuweather.async_get_daily_forecast() + except EXCEPTIONS as error: + raise UpdateFailed(error) from error + + _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) + + return result + + +def _get_device_info(location_key: str, name: str) -> DeviceInfo: + """Get device info.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, location_key)}, + manufacturer=MANUFACTURER, + name=name, + # You don't need to provide specific details for the URL, + # so passing in _ characters is fine if the location key + # is correct + configuration_url=( + "http://accuweather.com/en/" + f"_/_/{location_key}/weather-forecast/{location_key}/" + ), + ) diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py index e7bc41eaaf2..810638a1e49 100644 --- a/homeassistant/components/accuweather/diagnostics.py +++ b/homeassistant/components/accuweather/diagnostics.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from . import AccuWeatherDataUpdateCoordinator +from . import AccuWeatherData from .const import DOMAIN TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} @@ -19,13 +19,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + accuweather_data: AccuWeatherData = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), - "coordinator_data": coordinator.data, + "observation_data": accuweather_data.coordinator_observation.data, } - - return diagnostics_data diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index fa651d98efd..24a8180eef8 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["accuweather"], "quality_scale": "platinum", - "requirements": ["accuweather==2.1.1"], + "requirements": ["accuweather==3.0.0"], "single_config_entry": true } diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 521dfdfbead..95274297828 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -28,13 +28,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AccuWeatherDataUpdateCoordinator +from . import AccuWeatherData from .const import ( API_METRIC, ATTR_CATEGORY, ATTR_DIRECTION, ATTR_ENGLISH, - ATTR_FORECAST, ATTR_LEVEL, ATTR_SPEED, ATTR_VALUE, @@ -42,6 +41,10 @@ from .const import ( DOMAIN, MAX_FORECAST_DAYS, ) +from .coordinator import ( + AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherObservationDataUpdateCoordinator, +) PARALLEL_UPDATES = 1 @@ -52,12 +55,18 @@ class AccuWeatherSensorDescription(SensorEntityDescription): value_fn: Callable[[dict[str, Any]], str | int | float | None] attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} - day: int | None = None -FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( +@dataclass(frozen=True, kw_only=True) +class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription): + """Class describing AccuWeather sensor entities.""" + + day: int + + +FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = ( *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="AirQuality", icon="mdi:air-filter", value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), @@ -69,7 +78,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="CloudCoverDay", icon="mdi:weather-cloudy", entity_registry_enabled_default=False, @@ -81,7 +90,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="CloudCoverNight", icon="mdi:weather-cloudy", entity_registry_enabled_default=False, @@ -93,7 +102,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Grass", icon="mdi:grass", entity_registry_enabled_default=False, @@ -106,7 +115,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="HoursOfSun", icon="mdi:weather-partly-cloudy", native_unit_of_measurement=UnitOfTime.HOURS, @@ -117,7 +126,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="LongPhraseDay", value_fn=lambda data: cast(str, data), translation_key=f"condition_day_{day}d", @@ -126,7 +135,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="LongPhraseNight", value_fn=lambda data: cast(str, data), translation_key=f"condition_night_{day}d", @@ -135,7 +144,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Mold", icon="mdi:blur", entity_registry_enabled_default=False, @@ -148,7 +157,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Ragweed", icon="mdi:sprout", native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -161,7 +170,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureMax", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -172,7 +181,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureMin", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -183,7 +192,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureShadeMax", device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, @@ -195,7 +204,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureShadeMin", device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, @@ -207,7 +216,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="SolarIrradianceDay", icon="mdi:weather-sunny", entity_registry_enabled_default=False, @@ -219,7 +228,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="SolarIrradianceNight", icon="mdi:weather-sunny", entity_registry_enabled_default=False, @@ -231,7 +240,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="ThunderstormProbabilityDay", icon="mdi:weather-lightning", native_unit_of_measurement=PERCENTAGE, @@ -242,7 +251,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="ThunderstormProbabilityNight", icon="mdi:weather-lightning", native_unit_of_measurement=PERCENTAGE, @@ -253,7 +262,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Tree", icon="mdi:tree-outline", native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -266,7 +275,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="UVIndex", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, @@ -278,7 +287,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindGustDay", device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -291,7 +300,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindGustNight", device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -304,7 +313,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindDay", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -316,7 +325,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindNight", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -453,25 +462,33 @@ async def async_setup_entry( ) -> None: """Add AccuWeather entities from a config_entry.""" - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id] - sensors = [ - AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES + observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = ( + accuweather_data.coordinator_observation + ) + forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = ( + accuweather_data.coordinator_daily_forecast + ) + + sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [ + AccuWeatherSensor(observation_coordinator, description) + for description in SENSOR_TYPES ] - if coordinator.forecast: - for description in FORECAST_SENSOR_TYPES: - # Some air quality/allergy sensors are only available for certain - # locations. - if description.key not in coordinator.data[ATTR_FORECAST][description.day]: - continue - sensors.append(AccuWeatherSensor(coordinator, description)) + sensors.extend( + [ + AccuWeatherForecastSensor(forecast_daily_coordinator, description) + for description in FORECAST_SENSOR_TYPES + if description.key in forecast_daily_coordinator.data[description.day] + ] + ) async_add_entities(sensors) class AccuWeatherSensor( - CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity + CoordinatorEntity[AccuWeatherObservationDataUpdateCoordinator], SensorEntity ): """Define an AccuWeather entity.""" @@ -481,22 +498,15 @@ class AccuWeatherSensor( def __init__( self, - coordinator: AccuWeatherDataUpdateCoordinator, + coordinator: AccuWeatherObservationDataUpdateCoordinator, description: AccuWeatherSensorDescription, ) -> None: """Initialize.""" super().__init__(coordinator) - self.forecast_day = description.day + self.entity_description = description - self._sensor_data = _get_sensor_data( - coordinator.data, description.key, self.forecast_day - ) - if self.forecast_day is not None: - self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower() - else: - self._attr_unique_id = ( - f"{coordinator.location_key}-{description.key}".lower() - ) + self._sensor_data = self._get_sensor_data(coordinator.data, description.key) + self._attr_unique_id = f"{coordinator.location_key}-{description.key}".lower() self._attr_device_info = coordinator.device_info @property @@ -507,30 +517,78 @@ class AccuWeatherSensor( @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.forecast_day is not None: - return self.entity_description.attr_fn(self._sensor_data) - return self.entity_description.attr_fn(self.coordinator.data) @callback def _handle_coordinator_update(self) -> None: """Handle data update.""" - self._sensor_data = _get_sensor_data( + self._sensor_data = self._get_sensor_data( + self.coordinator.data, self.entity_description.key + ) + self.async_write_ha_state() + + @staticmethod + def _get_sensor_data( + sensors: dict[str, Any], + kind: str, + ) -> Any: + """Get sensor data.""" + if kind == "Precipitation": + return sensors["PrecipitationSummary"]["PastHour"] + + return sensors[kind] + + +class AccuWeatherForecastSensor( + CoordinatorEntity[AccuWeatherDailyForecastDataUpdateCoordinator], SensorEntity +): + """Define an AccuWeather entity.""" + + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + entity_description: AccuWeatherForecastSensorDescription + + def __init__( + self, + coordinator: AccuWeatherDailyForecastDataUpdateCoordinator, + description: AccuWeatherForecastSensorDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.forecast_day = description.day + self.entity_description = description + self._sensor_data = self._get_sensor_data( + coordinator.data, description.key, self.forecast_day + ) + self._attr_unique_id = ( + f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower() + ) + self._attr_device_info = coordinator.device_info + + @property + def native_value(self) -> str | int | float | None: + """Return the state.""" + return self.entity_description.value_fn(self._sensor_data) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return self.entity_description.attr_fn(self._sensor_data) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._sensor_data = self._get_sensor_data( self.coordinator.data, self.entity_description.key, self.forecast_day ) self.async_write_ha_state() - -def _get_sensor_data( - sensors: dict[str, Any], - kind: str, - forecast_day: int | None = None, -) -> Any: - """Get sensor data.""" - if forecast_day is not None: - return sensors[ATTR_FORECAST][forecast_day][kind] - - if kind == "Precipitation": - return sensors["PrecipitationSummary"]["PastHour"] - - return sensors[kind] + @staticmethod + def _get_sensor_data( + sensors: list[dict[str, Any]], + kind: str, + forecast_day: int, + ) -> Any: + """Get sensor data.""" + return sensors[forecast_day][kind] diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 718f2da6a75..9d8fce865fd 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -11,7 +11,7 @@ } }, "create_entry": { - "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options." + "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -790,16 +790,6 @@ } } }, - "options": { - "step": { - "init": { - "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", - "data": { - "forecast": "Weather forecast" - } - } - } - }, "system_health": { "info": { "can_reach_server": "Reach AccuWeather server", diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py index 607a557f333..f47828cb5a3 100644 --- a/homeassistant/components/accuweather/system_health.py +++ b/homeassistant/components/accuweather/system_health.py @@ -24,7 +24,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" remaining_requests = list(hass.data[DOMAIN].values())[ 0 - ].accuweather.requests_remaining + ].coordinator_observation.accuweather.requests_remaining return { "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT), diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 1f2e606f6ea..4d248a06ac3 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -17,8 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, + CoordinatorWeatherEntity, Forecast, - SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -31,19 +31,23 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util.dt import utc_from_timestamp -from . import AccuWeatherDataUpdateCoordinator +from . import AccuWeatherData from .const import ( API_METRIC, ATTR_DIRECTION, - ATTR_FORECAST, ATTR_SPEED, ATTR_VALUE, ATTRIBUTION, CONDITION_MAP, DOMAIN, ) +from .coordinator import ( + AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherObservationDataUpdateCoordinator, +) PARALLEL_UPDATES = 1 @@ -52,106 +56,134 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a AccuWeather weather entity from a config_entry.""" + accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id] - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - async_add_entities([AccuWeatherEntity(coordinator)]) + async_add_entities([AccuWeatherEntity(accuweather_data)]) class AccuWeatherEntity( - SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator] + CoordinatorWeatherEntity[ + AccuWeatherObservationDataUpdateCoordinator, + AccuWeatherDailyForecastDataUpdateCoordinator, + TimestampDataUpdateCoordinator, + TimestampDataUpdateCoordinator, + ] ): """Define an AccuWeather entity.""" _attr_has_entity_name = True _attr_name = None - def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: + def __init__(self, accuweather_data: AccuWeatherData) -> None: """Initialize.""" - super().__init__(coordinator) + super().__init__( + observation_coordinator=accuweather_data.coordinator_observation, + daily_coordinator=accuweather_data.coordinator_daily_forecast, + ) + self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_visibility_unit = UnitOfLength.KILOMETERS self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - self._attr_unique_id = coordinator.location_key + self._attr_unique_id = accuweather_data.coordinator_observation.location_key self._attr_attribution = ATTRIBUTION - self._attr_device_info = coordinator.device_info - if self.coordinator.forecast: - self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + self._attr_device_info = accuweather_data.coordinator_observation.device_info + self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + + self.observation_coordinator = accuweather_data.coordinator_observation + self.daily_coordinator = accuweather_data.coordinator_daily_forecast @property def condition(self) -> str | None: """Return the current condition.""" - return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"]) + return CONDITION_MAP.get(self.observation_coordinator.data["WeatherIcon"]) @property def cloud_coverage(self) -> float: """Return the Cloud coverage in %.""" - return cast(float, self.coordinator.data["CloudCover"]) + return cast(float, self.observation_coordinator.data["CloudCover"]) @property def native_apparent_temperature(self) -> float: """Return the apparent temperature.""" return cast( - float, self.coordinator.data["ApparentTemperature"][API_METRIC][ATTR_VALUE] + float, + self.observation_coordinator.data["ApparentTemperature"][API_METRIC][ + ATTR_VALUE + ], ) @property def native_temperature(self) -> float: """Return the temperature.""" - return cast(float, self.coordinator.data["Temperature"][API_METRIC][ATTR_VALUE]) + return cast( + float, + self.observation_coordinator.data["Temperature"][API_METRIC][ATTR_VALUE], + ) @property def native_pressure(self) -> float: """Return the pressure.""" - return cast(float, self.coordinator.data["Pressure"][API_METRIC][ATTR_VALUE]) + return cast( + float, self.observation_coordinator.data["Pressure"][API_METRIC][ATTR_VALUE] + ) @property def native_dew_point(self) -> float: """Return the dew point.""" - return cast(float, self.coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE]) + return cast( + float, self.observation_coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE] + ) @property def humidity(self) -> int: """Return the humidity.""" - return cast(int, self.coordinator.data["RelativeHumidity"]) + return cast(int, self.observation_coordinator.data["RelativeHumidity"]) @property def native_wind_gust_speed(self) -> float: """Return the wind gust speed.""" return cast( - float, self.coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ATTR_VALUE] + float, + self.observation_coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ + ATTR_VALUE + ], ) @property def native_wind_speed(self) -> float: """Return the wind speed.""" return cast( - float, self.coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ATTR_VALUE] + float, + self.observation_coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ + ATTR_VALUE + ], ) @property def wind_bearing(self) -> int: """Return the wind bearing.""" - return cast(int, self.coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"]) + return cast( + int, self.observation_coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"] + ) @property def native_visibility(self) -> float: """Return the visibility.""" - return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE]) + return cast( + float, + self.observation_coordinator.data["Visibility"][API_METRIC][ATTR_VALUE], + ) @property def uv_index(self) -> float: """Return the UV index.""" - return cast(float, self.coordinator.data["UVIndex"]) + return cast(float, self.observation_coordinator.data["UVIndex"]) @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 return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), @@ -175,5 +207,5 @@ class AccuWeatherEntity( ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]), } - for item in self.coordinator.data[ATTR_FORECAST] + for item in self.daily_coordinator.data ] diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 69b89cfe8cc..ac381ff46d5 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -135,11 +135,15 @@ class AdaxDevice(ClimateEntity): class LocalAdaxDevice(ClimateEntity): """Representation of a heater.""" - _attr_hvac_modes = [HVACMode.HEAT] + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_mode = HVACMode.HEAT _attr_max_temp = 35 _attr_min_temp = 5 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS @@ -152,6 +156,14 @@ class LocalAdaxDevice(ClimateEntity): manufacturer="Adax", ) + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + if hvac_mode == HVACMode.HEAT: + temperature = self._attr_target_temperature or self._attr_min_temp + await self._adax_data_handler.set_target_temperature(temperature) + elif hvac_mode == HVACMode.OFF: + await self._adax_data_handler.set_target_temperature(0) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: @@ -161,6 +173,14 @@ class LocalAdaxDevice(ClimateEntity): async def async_update(self) -> None: """Get the latest data.""" data = await self._adax_data_handler.get_status() - self._attr_target_temperature = data["target_temperature"] self._attr_current_temperature = data["current_temperature"] self._attr_available = self._attr_current_temperature is not None + if (target_temp := data["target_temperature"]) == 0: + self._attr_hvac_mode = HVACMode.OFF + self._attr_icon = "mdi:radiator-off" + if target_temp == 0: + self._attr_target_temperature = self._attr_min_temp + else: + self._attr_hvac_mode = HVACMode.HEAT + self._attr_icon = "mdi:radiator" + self._attr_target_temperature = target_temp diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index b3cbb3300bf..874a4cae963 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from dataclasses import dataclass + from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol @@ -24,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( CONF_FORCE, - DATA_ADGUARD_CLIENT, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -44,6 +45,14 @@ SERVICE_REFRESH_SCHEMA = vol.Schema( PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +@dataclass +class AdGuardData: + """Adguard data type.""" + + client: AdGuardHome + version: str + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AdGuard Home from a config entry.""" session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) @@ -57,13 +66,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard} - try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AdGuardData(adguard, version) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def add_url(call: ServiceCall) -> None: diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index 7b6827c19d4..5af739a8f0b 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -6,9 +6,6 @@ DOMAIN = "adguard" LOGGER = logging.getLogger(__package__) -DATA_ADGUARD_CLIENT = "adguard_client" -DATA_ADGUARD_VERSION = "adguard_version" - CONF_FORCE = "force" SERVICE_ADD_URL = "add_url" diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 8cb71a861e8..a4e16f1b995 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -2,13 +2,14 @@ from __future__ import annotations -from adguardhome import AdGuardHome, AdGuardHomeError +from adguardhome import AdGuardHomeError from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import Entity -from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from . import AdGuardData +from .const import DOMAIN, LOGGER class AdGuardHomeEntity(Entity): @@ -19,12 +20,13 @@ class AdGuardHomeEntity(Entity): def __init__( self, - adguard: AdGuardHome, + data: AdGuardData, entry: ConfigEntry, ) -> None: """Initialize the AdGuard Home entity.""" self._entry = entry - self.adguard = adguard + self.data = data + self.adguard = data.client async def async_update(self) -> None: """Update AdGuard Home entity.""" @@ -68,8 +70,6 @@ class AdGuardHomeEntity(Entity): }, manufacturer="AdGuard Team", name="AdGuard Home", - sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( - DATA_ADGUARD_VERSION - ), + sw_version=self.data.version, configuration_url=config_url, ) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 1e95a07bffa..ce112f49531 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -7,16 +7,16 @@ from dataclasses import dataclass from datetime import timedelta from typing import Any -from adguardhome import AdGuardHome, AdGuardHomeConnectionError +from adguardhome import AdGuardHome from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN +from . import AdGuardData +from .const import DOMAIN from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=300) @@ -89,17 +89,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up AdGuard Home sensor based on a config entry.""" - adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] - - try: - version = await adguard.version() - except AdGuardHomeConnectionError as exception: - raise PlatformNotReady from exception - - hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version + data: AdGuardData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [AdGuardHomeSensor(adguard, entry, description) for description in SENSORS], + [AdGuardHomeSensor(data, entry, description) for description in SENSORS], True, ) @@ -111,18 +104,18 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): def __init__( self, - adguard: AdGuardHome, + data: AdGuardData, entry: ConfigEntry, description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" - super().__init__(adguard, entry) + super().__init__(data, entry) self.entity_description = description self._attr_unique_id = "_".join( [ DOMAIN, - adguard.host, - str(adguard.port), + self.adguard.host, + str(self.adguard.port), "sensor", description.key, ] diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index ae4bee85d23..e084ed2f349 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -7,15 +7,15 @@ from dataclasses import dataclass from datetime import timedelta from typing import Any -from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from . import AdGuardData +from .const import DOMAIN, LOGGER from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=10) @@ -83,17 +83,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up AdGuard Home switch based on a config entry.""" - adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] - - try: - version = await adguard.version() - except AdGuardHomeConnectionError as exception: - raise PlatformNotReady from exception - - hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version + data: AdGuardData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES], + [AdGuardHomeSwitch(data, entry, description) for description in SWITCHES], True, ) @@ -105,15 +98,21 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity): def __init__( self, - adguard: AdGuardHome, + data: AdGuardData, entry: ConfigEntry, description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" - super().__init__(adguard, entry) + super().__init__(data, entry) self.entity_description = description self._attr_unique_id = "_".join( - [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] + [ + DOMAIN, + self.adguard.host, + str(self.adguard.port), + "switch", + description.key, + ] ) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/airly/diagnostics.py b/homeassistant/components/airly/diagnostics.py index 1d63fbc8277..d21d126c60e 100644 --- a/homeassistant/components/airly/diagnostics.py +++ b/homeassistant/components/airly/diagnostics.py @@ -26,9 +26,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "coordinator_data": coordinator.data, } - - return diagnostics_data diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index e8a2d492ae2..39617a8a019 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -44,10 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_method() -> AirthingsDevice: """Get data from Airthings BLE.""" - ble_device = bluetooth.async_ble_device_from_address(hass, address) - try: - data = await airthings.update_device(ble_device) # type: ignore[arg-type] + data = await airthings.update_device(ble_device) except Exception as err: raise UpdateFailed(f"Unable to fetch data: {err}") from err diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index 5f08f198761..d525aee04b1 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -87,7 +87,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error( "Unknown error occurred from %s: %s", discovery_info.address, err ) - raise err + raise return data async def async_step_bluetooth( diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index 6f17b9a317e..4b38923384a 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/airtouch5/config_flow.py b/homeassistant/components/airtouch5/config_flow.py index 65755350b47..3c4671cf54e 100644 --- a/homeassistant/components/airtouch5/config_flow.py +++ b/homeassistant/components/airtouch5/config_flow.py @@ -8,7 +8,7 @@ from typing import Any from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from .const import DOMAIN @@ -18,14 +18,14 @@ _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Airtouch 5.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index 83be481a4de..e53c01e0f81 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -16,7 +16,9 @@ from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.SELECT, Platform.SENSOR, + Platform.WATER_HEATER, ] diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index f53321ce353..8e8a7aff1bc 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -11,6 +11,7 @@ from aioairzone_cloud.const import ( AZD_AVAILABLE, AZD_FIRMWARE, AZD_GROUPS, + AZD_HOT_WATERS, AZD_INSTALLATIONS, AZD_NAME, AZD_SYSTEM_ID, @@ -136,6 +137,47 @@ class AirzoneGroupEntity(AirzoneEntity): self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) +class AirzoneHotWaterEntity(AirzoneEntity): + """Define an Airzone Cloud Hot Water entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + dhw_id: str, + dhw_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.dhw_id = dhw_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, dhw_id)}, + manufacturer=MANUFACTURER, + name=dhw_data[AZD_NAME], + via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]), + ) + + def get_airzone_value(self, key: str) -> Any: + """Return DHW value by key.""" + value = None + if dhw := self.coordinator.data[AZD_HOT_WATERS].get(self.dhw_id): + value = dhw.get(key) + return value + + async def _async_update_params(self, params: dict[str, Any]) -> None: + """Send DHW parameters to Cloud API.""" + _LOGGER.debug("dhw=%s: update_params=%s", self.entity_id, params) + try: + await self.coordinator.airzone.api_set_dhw_id_params(self.dhw_id, params) + except AirzoneCloudError as error: + raise HomeAssistantError( + f"Failed to set {self.entity_id} params: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + + class AirzoneInstallationEntity(AirzoneEntity): """Define an Airzone Cloud Installation entity.""" diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index b4445f6fe45..366f8214bc1 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.4.7"] + "requirements": ["aioairzone-cloud==0.5.1"] } diff --git a/homeassistant/components/airzone_cloud/select.py b/homeassistant/components/airzone_cloud/select.py new file mode 100644 index 00000000000..c5c9f664503 --- /dev/null +++ b/homeassistant/components/airzone_cloud/select.py @@ -0,0 +1,124 @@ +"""Support for the Airzone Cloud select.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Final + +from aioairzone_cloud.common import AirQualityMode +from aioairzone_cloud.const import ( + API_AQ_MODE_CONF, + API_VALUE, + AZD_AQ_MODE_CONF, + AZD_ZONES, +) + +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 DOMAIN +from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneZoneEntity + + +@dataclass(frozen=True, kw_only=True) +class AirzoneSelectDescription(SelectEntityDescription): + """Class to describe an Airzone select entity.""" + + api_param: str + options_dict: dict[str, str] + + +AIR_QUALITY_MAP: Final[dict[str, str]] = { + "off": AirQualityMode.OFF, + "on": AirQualityMode.ON, + "auto": AirQualityMode.AUTO, +} + + +ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( + AirzoneSelectDescription( + api_param=API_AQ_MODE_CONF, + entity_category=EntityCategory.CONFIG, + key=AZD_AQ_MODE_CONF, + options=list(AIR_QUALITY_MAP), + options_dict=AIR_QUALITY_MAP, + translation_key="air_quality", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone Cloud select from a config_entry.""" + coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + # Zones + async_add_entities( + AirzoneZoneSelect( + coordinator, + description, + zone_id, + zone_data, + ) + for description in ZONE_SELECT_TYPES + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items() + if description.key in zone_data + ) + + +class AirzoneBaseSelect(AirzoneEntity, SelectEntity): + """Define an Airzone Cloud select.""" + + entity_description: AirzoneSelectDescription + values_dict: dict[str, str] + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + def _get_current_option(self) -> str | None: + """Get current selected option.""" + value = self.get_airzone_value(self.entity_description.key) + return self.values_dict.get(value) + + @callback + def _async_update_attrs(self) -> None: + """Update select attributes.""" + self._attr_current_option = self._get_current_option() + + +class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect): + """Define an Airzone Cloud Zone select.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneSelectDescription, + zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, zone_id, zone_data) + + self._attr_unique_id = f"{zone_id}_{description.key}" + self.entity_description = description + self.values_dict = {v: k for k, v in description.options_dict.items()} + + self._async_update_attrs() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + param = self.entity_description.api_param + value = self.entity_description.options_dict[option] + params: dict[str, Any] = {} + params[param] = { + API_VALUE: value, + } + await self._async_update_params(params) diff --git a/homeassistant/components/airzone_cloud/strings.json b/homeassistant/components/airzone_cloud/strings.json index fe7c38c8374..fe9455aa69e 100644 --- a/homeassistant/components/airzone_cloud/strings.json +++ b/homeassistant/components/airzone_cloud/strings.json @@ -21,6 +21,16 @@ "air_quality_active": { "name": "Air Quality active" } + }, + "select": { + "air_quality": { + "name": "Air Quality mode", + "state": { + "off": "Off", + "on": "On", + "auto": "Auto" + } + } } } } diff --git a/homeassistant/components/airzone_cloud/water_heater.py b/homeassistant/components/airzone_cloud/water_heater.py new file mode 100644 index 00000000000..fd1c772b38a --- /dev/null +++ b/homeassistant/components/airzone_cloud/water_heater.py @@ -0,0 +1,164 @@ +"""Support for the Airzone Cloud water heater.""" + +from __future__ import annotations + +from typing import Any, Final + +from aioairzone_cloud.common import HotWaterOperation, TemperatureUnit +from aioairzone_cloud.const import ( + API_OPTS, + API_POWER, + API_POWERFUL_MODE, + API_SETPOINT, + API_UNITS, + API_VALUE, + AZD_HOT_WATERS, + AZD_OPERATION, + AZD_OPERATIONS, + AZD_TEMP, + AZD_TEMP_SET, + AZD_TEMP_SET_MAX, + AZD_TEMP_SET_MIN, +) + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneHotWaterEntity + +OPERATION_LIB_TO_HASS: Final[dict[HotWaterOperation, str]] = { + HotWaterOperation.Off: STATE_OFF, + HotWaterOperation.On: STATE_ECO, + HotWaterOperation.Powerful: STATE_PERFORMANCE, +} + +OPERATION_MODE_TO_DHW_PARAMS: Final[dict[str, dict[str, Any]]] = { + STATE_OFF: { + API_POWER: { + API_VALUE: False, + }, + }, + STATE_ECO: { + API_POWER: { + API_VALUE: True, + }, + API_POWERFUL_MODE: { + API_VALUE: False, + }, + }, + STATE_PERFORMANCE: { + API_POWER: { + API_VALUE: True, + }, + API_POWERFUL_MODE: { + API_VALUE: True, + }, + }, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone Cloud Water Heater from a config_entry.""" + coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + AirzoneWaterHeater( + coordinator, + dhw_id, + dhw_data, + ) + for dhw_id, dhw_data in coordinator.data.get(AZD_HOT_WATERS, {}).items() + ) + + +class AirzoneWaterHeater(AirzoneHotWaterEntity, WaterHeaterEntity): + """Define an Airzone Cloud Water Heater.""" + + _attr_name = None + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.OPERATION_MODE + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + dhw_id: str, + dhw_data: dict, + ) -> None: + """Initialize Airzone Cloud Water Heater.""" + super().__init__(coordinator, dhw_id, dhw_data) + + self._attr_unique_id = dhw_id + self._attr_operation_list = [ + OPERATION_LIB_TO_HASS[operation] + for operation in self.get_airzone_value(AZD_OPERATIONS) + ] + + self._async_update_attrs() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + params = { + API_POWER: { + API_VALUE: False, + }, + } + await self._async_update_params(params) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + params = { + API_POWER: { + API_VALUE: True, + }, + } + await self._async_update_params(params) + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new target operation mode.""" + params = OPERATION_MODE_TO_DHW_PARAMS.get(operation_mode, {}) + await self._async_update_params(params) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + params: dict[str, Any] = {} + if ATTR_TEMPERATURE in kwargs: + params[API_SETPOINT] = { + API_VALUE: kwargs[ATTR_TEMPERATURE], + API_OPTS: { + API_UNITS: TemperatureUnit.CELSIUS.value, + }, + } + await self._async_update_params(params) + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update water heater attributes.""" + self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) + self._attr_current_operation = OPERATION_LIB_TO_HASS[ + self.get_airzone_value(AZD_OPERATION) + ] + self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX) + self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN) + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index df822086db7..e960138853a 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -41,8 +41,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: ) try: await acc.login() - except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex: - raise ex + except (ClientError, TimeoutError, Aladdin.ConnectionError): + raise except Aladdin.InvalidPasswordError as ex: raise InvalidAuth from ex diff --git a/homeassistant/components/aladdin_connect/diagnostics.py b/homeassistant/components/aladdin_connect/diagnostics.py index b838ff79da3..67a31079f14 100644 --- a/homeassistant/components/aladdin_connect/diagnostics.py +++ b/homeassistant/components/aladdin_connect/diagnostics.py @@ -23,8 +23,6 @@ async def async_get_config_entry_diagnostics( acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "doors": async_redact_data(acc.doors, TO_REDACT), } - - return diagnostics_data diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 63c095ea6ce..3260454826a 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, Final, final +from typing import Any, Final, final import voluptuous as vol @@ -50,11 +50,6 @@ from .const import ( # noqa: F401 CodeFormat, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER: Final = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=30) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 471d32227c2..1ffeb7c73ac 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -26,13 +26,12 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HassJob, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant from homeassistant.exceptions import ServiceNotFound import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ecb7d5cb5a8..df32220895d 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -300,6 +300,10 @@ class Alexa(AlexaCapability): The API suggests you should explicitly include this interface. https://developer.amazon.com/docs/device-apis/alexa-interface.html + + To compare current supported locales in Home Assistant + with Alexa supported locales, run the following script: + python -m script.alexa_locales """ supported_locales = { @@ -1764,10 +1768,7 @@ class AlexaRangeController(AlexaCapability): speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) speed = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED) if speed_list is not None and speed is not None: - speed_index = next( - (i for i, v in enumerate(speed_list) if v == speed), None - ) - return speed_index + return next((i for i, v in enumerate(speed_list) if v == speed), None) # Valve Position if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}": diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index fb589dde566..0801a32a607 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -13,6 +13,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.storage import Store from .const import DOMAIN +from .entities import TRANSLATION_TABLE from .state_report import async_enable_proactive_mode STORE_AUTHORIZED = "authorized" @@ -101,6 +102,10 @@ class AbstractConfig(ABC): """If an entity should be exposed.""" return False + def generate_alexa_id(self, entity_id: str) -> str: + """Return the alexa ID for an entity ID.""" + return entity_id.replace(".", "#").translate(TRANSLATION_TABLE) + @callback def async_invalidate_access_token(self) -> None: """Invalidate access token.""" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 61ab220c60c..ca7b78f7ff5 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -259,11 +259,6 @@ class DisplayCategory: WEARABLE = "WEARABLE" -def generate_alexa_id(entity_id: str) -> str: - """Return the alexa ID for an entity ID.""" - return entity_id.replace(".", "#").translate(TRANSLATION_TABLE) - - class AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. @@ -298,7 +293,7 @@ class AlexaEntity: def alexa_id(self) -> str: """Return the Alexa API entity id.""" - return generate_alexa_id(self.entity.entity_id) + return self.config.generate_alexa_id(self.entity.entity_id) def display_categories(self) -> list[str] | None: """Return a list of display categories.""" @@ -384,10 +379,8 @@ def async_get_entities( try: alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state) interfaces = list(alexa_entity.interfaces()) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception( - "Unable to serialize %s for discovery: %s", state.entity_id, exc - ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unable to serialize %s for discovery", state.entity_id) else: if not interfaces: continue diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 30c2fecccf8..c28b1923399 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -126,9 +126,9 @@ async def async_api_discovery( continue try: discovered_serialized_entity = alexa_entity.serialize_discovery() - except Exception as exc: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unable to serialize %s for discovery: %s", alexa_entity.entity_id, exc + "Unable to serialize %s for discovery", alexa_entity.entity_id ) else: discovery_endpoints.append(discovered_serialized_entity) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 8d266e4a634..217d5dccc25 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -1,5 +1,6 @@ """Support for Alexa skill service end point.""" +from collections.abc import Callable, Coroutine import enum import logging from typing import Any @@ -16,7 +17,9 @@ from .const import DOMAIN, SYN_RESOLUTION_MATCH _LOGGER = logging.getLogger(__name__) -HANDLERS = Registry() # type: ignore[var-annotated] +HANDLERS: Registry[ + str, Callable[[HomeAssistant, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]]] +] = Registry() INTENTS_API_ENDPOINT = "/api/alexa" @@ -94,8 +97,8 @@ class AlexaIntentsView(http.HomeAssistantView): ) ) - except intent.IntentError as err: - _LOGGER.exception(str(err)) + except intent.IntentError: + _LOGGER.exception("Error handling intent") return self.json( intent_error_response(hass, message, "Error handling intent.") ) @@ -129,8 +132,7 @@ async def async_handle_message( if not (handler := HANDLERS.get(req_type)): raise UnknownRequest(f"Received unknown request {req_type}") - response: dict[str, Any] = await handler(hass, message) - return response + return await handler(hass, message) @HANDLERS.register("SessionEndedRequest") diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 4bc63f6ccae..7782716798a 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -291,9 +291,9 @@ class AlexaPresetResource(AlexaCapabilityResource): def __init__( self, labels: list[str], - min_value: int | float, - max_value: int | float, - precision: int | float, + min_value: float, + max_value: float, + precision: float, unit: str | None = None, ) -> None: """Initialize an Alexa presetResource.""" @@ -306,7 +306,7 @@ class AlexaPresetResource(AlexaCapabilityResource): if unit in AlexaGlobalCatalog.__dict__.values(): self._unit_of_measure = unit - def add_preset(self, value: int | float, labels: list[str]) -> None: + def add_preset(self, value: float, labels: list[str]) -> None: """Add preset to configuration presets array.""" self._presets.append({"value": value, "labels": labels}) @@ -405,7 +405,7 @@ class AlexaSemantics: ) def add_states_to_range( - self, states: list[str], min_value: int | float, max_value: int | float + self, states: list[str], min_value: float, max_value: float ) -> None: """Add StatesToRange stateMappings.""" self._add_state_mapping( diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 9c640d76dd4..dc6c8ee3186 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -13,10 +13,16 @@ from uuid import uuid4 import aiohttp from homeassistant.components import event -from homeassistant.const import MATCH_ALL, STATE_ON -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback +from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util from homeassistant.util.json import JsonObjectType, json_loads_object @@ -35,7 +41,7 @@ from .const import ( Cause, ) from .diagnostics import async_redact_auth_data -from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id +from .entities import ENTITY_ADAPTERS, AlexaEntity from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink if TYPE_CHECKING: @@ -265,28 +271,35 @@ async def async_enable_proactive_mode( checker = await create_checker(hass, DOMAIN, extra_significant_check) - async def async_entity_state_listener( - changed_entity: str, - old_state: State | None, - new_state: State | None, - ) -> None: + @callback + def _async_entity_state_filter(data: EventStateChangedData) -> bool: if not hass.is_running: - return + return False - if not new_state: - return + if not (new_state := data["new_state"]): + return False if new_state.domain not in ENTITY_ADAPTERS: - return + return False + changed_entity = data["entity_id"] if not smart_home_config.should_expose(changed_entity): _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) - return + return False + + return True + + async def _async_entity_state_listener( + event_: Event[EventStateChangedData], + ) -> None: + data = event_.data + new_state = data["new_state"] + if TYPE_CHECKING: + assert new_state is not None alexa_changed_entity: AlexaEntity = ENTITY_ADAPTERS[new_state.domain]( hass, smart_home_config, new_state ) - # Determine how entity should be reported on should_report = False should_doorbell = False @@ -303,6 +316,7 @@ async def async_enable_proactive_mode( return if should_doorbell: + old_state = data["old_state"] if ( new_state.domain == event.DOMAIN or new_state.state == STATE_ON @@ -324,7 +338,11 @@ async def async_enable_proactive_mode( hass, smart_home_config, alexa_changed_entity, alexa_properties ) - return async_track_state_change(hass, MATCH_ALL, async_entity_state_listener) + return hass.bus.async_listen( + EVENT_STATE_CHANGED, + _async_entity_state_listener, + event_filter=_async_entity_state_filter, + ) async def async_send_changereport_message( @@ -474,7 +492,7 @@ async def async_send_delete_message( if domain not in ENTITY_ADAPTERS: continue - endpoints.append({"endpointId": generate_alexa_id(entity_id)}) + endpoints.append({"endpointId": config.generate_alexa_id(entity_id)}) payload: dict[str, Any] = { "endpoints": endpoints, diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 55137b58832..803bf8b80aa 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], - "requirements": ["boto3==1.33.13"] + "requirements": ["boto3==1.34.51"] } diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index 174e8716e0b..a94700c27d1 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -60,10 +60,6 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): try: sites: list[Site] = filter_sites(api.get_sites()) - if len(sites) == 0: - self._errors[CONF_API_TOKEN] = "no_site" - return None - return sites except amberelectric.ApiException as api_exception: if api_exception.status == 403: self._errors[CONF_API_TOKEN] = "invalid_api_token" @@ -71,6 +67,11 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): self._errors[CONF_API_TOKEN] = "unknown_error" return None + if len(sites) == 0: + self._errors[CONF_API_TOKEN] = "no_site" + return None + return sites + async def async_step_user( self, user_input: dict[str, str] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/ambient_network/__init__.py b/homeassistant/components/ambient_network/__init__.py new file mode 100644 index 00000000000..b286fb7fbc9 --- /dev/null +++ b/homeassistant/components/ambient_network/__init__.py @@ -0,0 +1,35 @@ +"""The Ambient Weather Network integration.""" + +from __future__ import annotations + +from aioambient.open_api import OpenAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import AmbientNetworkDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Ambient Weather Network from a config entry.""" + + api = OpenAPI() + coordinator = AmbientNetworkDataUpdateCoordinator(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 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/ambient_network/config_flow.py b/homeassistant/components/ambient_network/config_flow.py new file mode 100644 index 00000000000..d29134db1c9 --- /dev/null +++ b/homeassistant/components/ambient_network/config_flow.py @@ -0,0 +1,152 @@ +"""Config flow for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from typing import Any + +from aioambient import OpenAPI +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_MAC, + CONF_RADIUS, + UnitOfLength, +) +from homeassistant.helpers.selector import ( + LocationSelector, + LocationSelectorConfig, + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) +from homeassistant.util.unit_conversion import DistanceConverter + +from .const import API_STATION_INDOOR, API_STATION_INFO, API_STATION_MAC_ADDRESS, DOMAIN +from .helper import get_station_name + +CONF_USER = "user" +CONF_STATION = "station" + +# One mile +CONF_RADIUS_DEFAULT = 1609.34 + + +class AmbientNetworkConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle the config flow for the Ambient Weather Network integration.""" + + VERSION = 1 + + def __init__(self) -> None: + """Construct the config flow.""" + + self._longitude = 0.0 + self._latitude = 0.0 + self._radius = 0.0 + self._stations: dict[str, dict[str, Any]] = {} + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: + """Handle the initial step to select the location.""" + + errors: dict[str, str] | None = None + if user_input: + self._latitude = user_input[CONF_LOCATION][CONF_LATITUDE] + self._longitude = user_input[CONF_LOCATION][CONF_LONGITUDE] + self._radius = user_input[CONF_LOCATION][CONF_RADIUS] + + client: OpenAPI = OpenAPI() + self._stations = { + x[API_STATION_MAC_ADDRESS]: x + for x in await client.get_devices_by_location( + self._latitude, + self._longitude, + radius=DistanceConverter.convert( + self._radius, + UnitOfLength.METERS, + UnitOfLength.MILES, + ), + ) + } + + # Filter out indoor stations + self._stations = dict( + filter( + lambda item: not item[1] + .get(API_STATION_INFO, {}) + .get(API_STATION_INDOOR, False), + self._stations.items(), + ) + ) + + if self._stations: + return await self.async_step_station() + + errors = {"base": "no_stations_found"} + + schema: vol.Schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_LOCATION, + ): LocationSelector(LocationSelectorConfig(radius=True)), + } + ), + { + CONF_LOCATION: { + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + CONF_RADIUS: CONF_RADIUS_DEFAULT, + } + if not errors + else { + CONF_LATITUDE: self._latitude, + CONF_LONGITUDE: self._longitude, + CONF_RADIUS: self._radius, + } + }, + ) + + return self.async_show_form( + step_id=CONF_USER, data_schema=schema, errors=errors if errors else {} + ) + + async def async_step_station( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the second step to select the station.""" + + if user_input: + mac_address = user_input[CONF_STATION] + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=get_station_name(self._stations[mac_address]), + data={CONF_MAC: mac_address}, + ) + + options: list[SelectOptionDict] = [ + SelectOptionDict( + label=get_station_name(station), + value=mac_address, + ) + for mac_address, station in self._stations.items() + ] + + schema: vol.Schema = vol.Schema( + { + vol.Required(CONF_STATION): SelectSelector( + SelectSelectorConfig(options=options, multiple=False, sort=True), + ) + } + ) + + return self.async_show_form( + step_id=CONF_STATION, + data_schema=schema, + ) diff --git a/homeassistant/components/ambient_network/const.py b/homeassistant/components/ambient_network/const.py new file mode 100644 index 00000000000..402e5f81097 --- /dev/null +++ b/homeassistant/components/ambient_network/const.py @@ -0,0 +1,16 @@ +"""Constants for the Ambient Weather Network integration.""" + +import logging + +DOMAIN = "ambient_network" + +API_LAST_DATA = "lastData" +API_STATION_COORDS = "coords" +API_STATION_INDOOR = "indoor" +API_STATION_INFO = "info" +API_STATION_LOCATION = "location" +API_STATION_NAME = "name" +API_STATION_MAC_ADDRESS = "macAddress" +API_STATION_TYPE = "stationtype" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/ambient_network/coordinator.py b/homeassistant/components/ambient_network/coordinator.py new file mode 100644 index 00000000000..f26ddd47b24 --- /dev/null +++ b/homeassistant/components/ambient_network/coordinator.py @@ -0,0 +1,65 @@ +"""DataUpdateCoordinator for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Any, cast + +from aioambient import OpenAPI +from aioambient.errors import RequestError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import API_LAST_DATA, DOMAIN, LOGGER +from .helper import get_station_name + +SCAN_INTERVAL = timedelta(minutes=5) + + +class AmbientNetworkDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """The Ambient Network Data Update Coordinator.""" + + config_entry: ConfigEntry + station_name: str + + def __init__(self, hass: HomeAssistant, api: OpenAPI) -> None: + """Initialize the coordinator.""" + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + self.api = api + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch the latest data from the Ambient Network.""" + + try: + response = await self.api.get_device_details( + self.config_entry.data[CONF_MAC] + ) + except RequestError as ex: + raise UpdateFailed("Cannot connect to Ambient Network") from ex + + self.station_name = get_station_name(response) + + if (last_data := response.get(API_LAST_DATA)) is None: + raise UpdateFailed( + f"Station '{self.config_entry.title}' did not report any data" + ) + + # Eliminate data if the station hasn't been updated for a while. + if (created_at := last_data.get("created_at")) is None: + raise UpdateFailed( + f"Station '{self.config_entry.title}' did not report a time stamp" + ) + + # Eliminate data that has been generated more than an hour ago. The station is + # probably offline. + if int(created_at / 1000) < int( + (datetime.now() - timedelta(hours=1)).timestamp() + ): + raise UpdateFailed( + f"Station '{self.config_entry.title}' reported stale data" + ) + + return cast(dict[str, Any], last_data) diff --git a/homeassistant/components/ambient_network/entity.py b/homeassistant/components/ambient_network/entity.py new file mode 100644 index 00000000000..ad0241ea3de --- /dev/null +++ b/homeassistant/components/ambient_network/entity.py @@ -0,0 +1,50 @@ +"""Base entity class for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from abc import abstractmethod + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AmbientNetworkDataUpdateCoordinator + + +class AmbientNetworkEntity(CoordinatorEntity[AmbientNetworkDataUpdateCoordinator]): + """Entity class for Ambient network devices.""" + + _attr_attribution = "Data provided by ambientnetwork.net" + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AmbientNetworkDataUpdateCoordinator, + description: EntityDescription, + mac_address: str, + ) -> None: + """Initialize the Ambient network entity.""" + + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{mac_address}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + name=coordinator.station_name, + identifiers={(DOMAIN, mac_address)}, + manufacturer="Ambient Weather", + ) + self._update_attrs() + + @abstractmethod + def _update_attrs(self) -> None: + """Update state attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Get the latest data and updates the state.""" + + self._update_attrs() + super()._handle_coordinator_update() diff --git a/homeassistant/components/ambient_network/helper.py b/homeassistant/components/ambient_network/helper.py new file mode 100644 index 00000000000..fbde45ee756 --- /dev/null +++ b/homeassistant/components/ambient_network/helper.py @@ -0,0 +1,31 @@ +"""Helper class for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from typing import Any + +from .const import ( + API_LAST_DATA, + API_STATION_COORDS, + API_STATION_INFO, + API_STATION_LOCATION, + API_STATION_NAME, + API_STATION_TYPE, +) + + +def get_station_name(station: dict[str, Any]) -> str: + """Pick a station name. + + Station names can be empty, in which case we construct the name from + the location and device type. + """ + if name := station.get(API_STATION_INFO, {}).get(API_STATION_NAME): + return str(name) + location = ( + station.get(API_STATION_INFO, {}) + .get(API_STATION_COORDS, {}) + .get(API_STATION_LOCATION) + ) + station_type = station.get(API_LAST_DATA, {}).get(API_STATION_TYPE) + return f"{location}{'' if location is None or station_type is None else ' '}{station_type}" diff --git a/homeassistant/components/ambient_network/icons.json b/homeassistant/components/ambient_network/icons.json new file mode 100644 index 00000000000..a7abebce187 --- /dev/null +++ b/homeassistant/components/ambient_network/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "last_rain": { + "default": "mdi:water" + }, + "lightning_strikes_per_day": { + "default": "mdi:lightning-bolt" + }, + "lightning_strikes_per_hour": { + "default": "mdi:lightning-bolt" + }, + "lightning_distance": { + "default": "mdi:lightning-bolt" + }, + "wind_direction": { + "default": "mdi:compass-outline" + } + } + } +} diff --git a/homeassistant/components/ambient_network/manifest.json b/homeassistant/components/ambient_network/manifest.json new file mode 100644 index 00000000000..553adb240b0 --- /dev/null +++ b/homeassistant/components/ambient_network/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "ambient_network", + "name": "Ambient Weather Network", + "codeowners": ["@thomaskistler"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ambient_network", + "integration_type": "service", + "iot_class": "cloud_polling", + "loggers": ["aioambient"], + "requirements": ["aioambient==2024.01.0"] +} diff --git a/homeassistant/components/ambient_network/sensor.py b/homeassistant/components/ambient_network/sensor.py new file mode 100644 index 00000000000..c28b69229d8 --- /dev/null +++ b/homeassistant/components/ambient_network/sensor.py @@ -0,0 +1,315 @@ +"""Support for Ambient Weather Network sensors.""" + +from __future__ import annotations + +from datetime import datetime + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + CONF_MAC, + DEGREE, + PERCENTAGE, + UnitOfIrradiance, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolumetricFlux, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util + +from .const import DOMAIN +from .coordinator import AmbientNetworkDataUpdateCoordinator +from .entity import AmbientNetworkEntity + +TYPE_AQI_PM25 = "aqi_pm25" +TYPE_AQI_PM25_24H = "aqi_pm25_24h" +TYPE_BAROMABSIN = "baromabsin" +TYPE_BAROMRELIN = "baromrelin" +TYPE_CO2 = "co2" +TYPE_DAILYRAININ = "dailyrainin" +TYPE_DEWPOINT = "dewPoint" +TYPE_EVENTRAININ = "eventrainin" +TYPE_FEELSLIKE = "feelsLike" +TYPE_HOURLYRAININ = "hourlyrainin" +TYPE_HUMIDITY = "humidity" +TYPE_LASTRAIN = "lastRain" +TYPE_LIGHTNING_DISTANCE = "lightning_distance" +TYPE_LIGHTNING_PER_DAY = "lightning_day" +TYPE_LIGHTNING_PER_HOUR = "lightning_hour" +TYPE_MAXDAILYGUST = "maxdailygust" +TYPE_MONTHLYRAININ = "monthlyrainin" +TYPE_PM25 = "pm25" +TYPE_PM25_24H = "pm25_24h" +TYPE_SOLARRADIATION = "solarradiation" +TYPE_TEMPF = "tempf" +TYPE_UV = "uv" +TYPE_WEEKLYRAININ = "weeklyrainin" +TYPE_WINDDIR = "winddir" +TYPE_WINDGUSTMPH = "windgustmph" +TYPE_WINDSPEEDMPH = "windspeedmph" +TYPE_YEARLYRAININ = "yearlyrainin" + + +SENSOR_DESCRIPTIONS = ( + SensorEntityDescription( + key=TYPE_AQI_PM25, + translation_key="pm25_aqi", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_24H, + translation_key="pm25_aqi_24h_average", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_BAROMABSIN, + translation_key="absolute_pressure", + native_unit_of_measurement=UnitOfPressure.INHG, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_BAROMRELIN, + translation_key="relative_pressure", + native_unit_of_measurement=UnitOfPressure.INHG, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + ), + SensorEntityDescription( + key=TYPE_CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_DAILYRAININ, + translation_key="daily_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + ), + SensorEntityDescription( + key=TYPE_DEWPOINT, + translation_key="dew_point", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_FEELSLIKE, + translation_key="feels_like", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_HOURLYRAININ, + translation_key="hourly_rain", + native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + suggested_display_precision=2, + ), + SensorEntityDescription( + key=TYPE_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_LASTRAIN, + translation_key="last_rain", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_DAY, + translation_key="lightning_strikes_per_day", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_HOUR, + translation_key="lightning_strikes_per_hour", + native_unit_of_measurement="strikes/hour", + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_DISTANCE, + translation_key="lightning_distance", + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_MAXDAILYGUST, + translation_key="max_daily_gust", + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_MONTHLYRAININ, + translation_key="monthly_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_PM25_24H, + translation_key="pm25_24h_average", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_SOLARRADIATION, + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + device_class=SensorDeviceClass.IRRADIANCE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_TEMPF, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_UV, + translation_key="uv_index", + native_unit_of_measurement="index", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_WEEKLYRAININ, + translation_key="weekly_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_WINDDIR, + translation_key="wind_direction", + native_unit_of_measurement=DEGREE, + suggested_display_precision=0, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_WINDGUSTMPH, + translation_key="wind_gust", + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_WINDSPEEDMPH, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_YEARLYRAININ, + translation_key="yearly_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Ambient Network sensor entities.""" + + coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if coordinator.config_entry is not None: + async_add_entities( + AmbientNetworkSensor( + coordinator, + description, + coordinator.config_entry.data[CONF_MAC], + ) + for description in SENSOR_DESCRIPTIONS + if coordinator.data.get(description.key) is not None + ) + + +class AmbientNetworkSensor(AmbientNetworkEntity, SensorEntity): + """A sensor implementation for an Ambient Weather Network sensor.""" + + def __init__( + self, + coordinator: AmbientNetworkDataUpdateCoordinator, + description: SensorEntityDescription, + mac_address: str, + ) -> None: + """Initialize a sensor object.""" + + super().__init__(coordinator, description, mac_address) + + def _update_attrs(self) -> None: + """Update sensor attributes.""" + + value = self.coordinator.data.get(self.entity_description.key) + + # Treatments for special units. + if value is not None and self.device_class == SensorDeviceClass.TIMESTAMP: + value = datetime.fromtimestamp(value / 1000, tz=dt_util.DEFAULT_TIME_ZONE) + + self._attr_available = value is not None + self._attr_native_value = value diff --git a/homeassistant/components/ambient_network/strings.json b/homeassistant/components/ambient_network/strings.json new file mode 100644 index 00000000000..7d18c40d902 --- /dev/null +++ b/homeassistant/components/ambient_network/strings.json @@ -0,0 +1,87 @@ +{ + "config": { + "step": { + "user": { + "title": "Select region", + "description": "Choose the region you want to survey in order to locate Ambient personal weather stations." + }, + "station": { + "title": "Select station", + "description": "Select the weather station you want to add to Home Assistant.", + "data": { + "station": "Station" + } + } + }, + "error": { + "no_stations_found": "Did not find any stations in the selected region." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "pm25_24h_average": { + "name": "PM2.5 (24 hour average)" + }, + "pm25_aqi": { + "name": "PM2.5 AQI" + }, + "pm25_aqi_24h_average": { + "name": "PM2.5 AQI (24 hour average)" + }, + "absolute_pressure": { + "name": "Absolute pressure" + }, + "relative_pressure": { + "name": "Relative pressure" + }, + "daily_rain": { + "name": "Daily rain" + }, + "dew_point": { + "name": "Dew point" + }, + "feels_like": { + "name": "Feels like" + }, + "hourly_rain": { + "name": "Hourly rain" + }, + "last_rain": { + "name": "Last rain" + }, + "lightning_strikes_per_day": { + "name": "Lightning strikes per day" + }, + "lightning_strikes_per_hour": { + "name": "Lightning strikes per hour" + }, + "lightning_distance": { + "name": "Lightning distance" + }, + "max_daily_gust": { + "name": "Max daily gust" + }, + "monthly_rain": { + "name": "Monthly rain" + }, + "uv_index": { + "name": "UV index" + }, + "weekly_rain": { + "name": "Weekly rain" + }, + "wind_direction": { + "name": "Wind direction" + }, + "wind_gust": { + "name": "Wind gust" + }, + "yearly_rain": { + "name": "Yearly rain" + } + } + } +} diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 0984e21a722..b55a7b866cc 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -93,7 +93,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: ambient = hass.data[DOMAIN].pop(entry.entry_id) - hass.async_create_task(ambient.ws_disconnect()) + hass.async_create_task(ambient.ws_disconnect(), eager_start=True) return unload_ok @@ -179,7 +179,8 @@ class AmbientStation: self._hass.async_create_task( self._hass.config_entries.async_forward_entry_setups( self._entry, PLATFORMS - ) + ), + eager_start=True, ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY diff --git a/homeassistant/components/ambient_station/entity.py b/homeassistant/components/ambient_station/entity.py index a1a81d97c3f..24dfab438d8 100644 --- a/homeassistant/components/ambient_station/entity.py +++ b/homeassistant/components/ambient_station/entity.py @@ -49,7 +49,7 @@ class AmbientWeatherEntity(Entity): last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA] key = self.entity_description.key available_key = TYPE_SOLARRADIATION if key == TYPE_SOLARRADIATION_LX else key - self._attr_available = last_data[available_key] is not None + self._attr_available = last_data.get(available_key) is not None self.update_from_latest_data() self.async_write_ha_state() diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index db729197a59..229ebee4fbf 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -19,6 +19,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, UnitOfIrradiance, + UnitOfLength, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, @@ -61,6 +62,8 @@ TYPE_HUMIDITYIN = "humidityin" TYPE_LASTRAIN = "lastRain" TYPE_LIGHTNING_PER_DAY = "lightning_day" TYPE_LIGHTNING_PER_HOUR = "lightning_hour" +TYPE_LASTLIGHTNING_DISTANCE = "lightning_distance" +TYPE_LASTLIGHTNING = "lightning_time" TYPE_MAXDAILYGUST = "maxdailygust" TYPE_MONTHLYRAININ = "monthlyrainin" TYPE_PM25 = "pm25" @@ -296,6 +299,18 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL, ), + SensorEntityDescription( + key=TYPE_LASTLIGHTNING, + translation_key="last_lightning_strike", + device_class=SensorDeviceClass.TIMESTAMP, + ), + SensorEntityDescription( + key=TYPE_LASTLIGHTNING_DISTANCE, + translation_key="last_lightning_strike_distance", + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, translation_key="max_gust", @@ -685,5 +700,9 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][key] if key == TYPE_LASTRAIN: self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z") + elif key == TYPE_LASTLIGHTNING: + self._attr_native_value = datetime.fromtimestamp( + raw / 1000, tz=UTC + ) # Ambient uses millisecond epoch else: self._attr_native_value = raw diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json index 02bceda500f..25006dce0e9 100644 --- a/homeassistant/components/ambient_station/strings.json +++ b/homeassistant/components/ambient_station/strings.json @@ -219,6 +219,12 @@ "last_rain": { "name": "Last rain" }, + "last_lightning_strike": { + "name": "Last Lightning strike" + }, + "last_lightning_strike_distance": { + "name": "Last Lightning strike distance" + }, "lightning_strikes_per_day": { "name": "Lightning strikes per day" }, diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index cb6abff3f89..c12aa6d7916 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -203,8 +203,7 @@ class AmcrestChecker(ApiWrapper): async def async_command(self, *args: Any, **kwargs: Any) -> httpx.Response: """amcrest.ApiWrapper.command wrapper to catch errors.""" async with self._async_command_wrapper(): - ret = await super().async_command(*args, **kwargs) - return ret + return await super().async_command(*args, **kwargs) @asynccontextmanager async def async_stream_command( diff --git a/homeassistant/components/analytics_insights/__init__.py b/homeassistant/components/analytics_insights/__init__.py index 65c3930e97d..79556fb68c2 100644 --- a/homeassistant/components/analytics_insights/__init__.py +++ b/homeassistant/components/analytics_insights/__init__.py @@ -49,9 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AnalyticsInsightsData( - coordinator=coordinator, names=names - ) + hass.data[DOMAIN] = AnalyticsInsightsData(coordinator=coordinator, names=names) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -62,7 +60,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.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) + hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index 30b8ca12579..cef5ac2e9e5 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -53,7 +53,6 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - self._async_abort_entries_match() errors: dict[str, str] = {} if user_input is not None: if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( diff --git a/homeassistant/components/analytics_insights/manifest.json b/homeassistant/components/analytics_insights/manifest.json index d33bb23b1b7..adf2d634ef8 100644 --- a/homeassistant/components/analytics_insights/manifest.json +++ b/homeassistant/components/analytics_insights/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["python_homeassistant_analytics"], - "requirements": ["python-homeassistant-analytics==0.6.0"] + "requirements": ["python-homeassistant-analytics==0.6.0"], + "single_config_entry": true } diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py index e776ddb9f41..ee1496eb52c 100644 --- a/homeassistant/components/analytics_insights/sensor.py +++ b/homeassistant/components/analytics_insights/sensor.py @@ -65,7 +65,7 @@ async def async_setup_entry( ) -> None: """Initialize the entries.""" - analytics_data: AnalyticsInsightsData = hass.data[DOMAIN][entry.entry_id] + analytics_data: AnalyticsInsightsData = hass.data[DOMAIN] coordinator: HomeassistantAnalyticsDataUpdateCoordinator = ( analytics_data.coordinator ) diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 6de1ab9dbe4..00c9cfa4404 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -13,8 +13,7 @@ } }, "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "error": { "no_integration_selected": "You must select at least one integration to track" diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index fb29c0d5e49..454b748dcc2 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -19,10 +19,9 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, 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 from homeassistant.util import ssl as ssl_util diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index bc214e56d80..77b2b8591e5 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -17,6 +17,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import APCUPSdCoordinator +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) _DESCRIPTION = BinarySensorEntityDescription( key="statflag", diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 008171cfe3c..6ac33072856 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -28,6 +28,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import APCUPSdCoordinator +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) SENSORS: dict[str, SensorEntityDescription] = { diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 82aaefe1288..73751daa6cb 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -37,7 +37,7 @@ from homeassistant.const import ( URL_API_TEMPLATE, ) import homeassistant.core as ha -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HomeAssistant from homeassistant.exceptions import ( InvalidEntityFormatError, InvalidStateError, @@ -46,10 +46,10 @@ from homeassistant.exceptions import ( Unauthorized, ) 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 +from homeassistant.util.event_type import EventType from homeassistant.util.json import json_loads _LOGGER = logging.getLogger(__name__) @@ -135,7 +135,7 @@ class APIEventStream(HomeAssistantView): stop_obj = object() to_write: asyncio.Queue[object | str] = asyncio.Queue() - restrict: list[str] | None = None + restrict: list[EventType[Any] | str] | None = None if restrict_str := request.query.get("restrict"): restrict = [*restrict_str.split(","), EVENT_HOMEASSISTANT_STOP] @@ -284,7 +284,8 @@ class APIEntityStateView(HomeAssistantView): # Read the state back for our response status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK - assert (state := hass.states.get(entity_id)) + state = hass.states.get(entity_id) + assert state resp = self.json(state.as_dict(), status_code) resp.headers.add("Location", f"/api/states/{entity_id}") @@ -398,7 +399,6 @@ class APIDomainServicesView(HomeAssistantView): cancel_listen = hass.bus.async_listen( EVENT_STATE_CHANGED, _async_save_changed_entities, - run_immediately=True, ) try: diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 9a72a89c876..cd1a1c59127 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -102,9 +102,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 19cbb24d8a2..1f2aa3b3b3a 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -76,9 +76,9 @@ async def device_scan( return None try: ip_address(identifier) - return [identifier] except ValueError: return None + return [identifier] # If we have an address, only probe that address to avoid # broadcast traffic on the network @@ -380,9 +380,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN): }, ) if entry.source != SOURCE_IGNORE: - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) + self.hass.config_entries.async_schedule_reload(entry.entry_id) if not allow_exist: raise DeviceAlreadyConfigured diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 822a9c3306a..aed2c0ae3f0 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -5,10 +5,14 @@ from collections.abc import Iterable import logging from typing import Any +from pyatv.const import InputAction + from homeassistant.components.remote import ( ATTR_DELAY_SECS, + ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, + DEFAULT_HOLD_SECS, RemoteEntity, ) from homeassistant.config_entries import ConfigEntry @@ -29,7 +33,6 @@ COMMAND_TO_ATTRIBUTE = { "turn_off": ("power", "turn_off"), "volume_up": ("audio", "volume_up"), "volume_down": ("audio", "volume_down"), - "home_hold": ("remote_control", "home"), } @@ -66,6 +69,7 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + hold_secs = kwargs.get(ATTR_HOLD_SECS, DEFAULT_HOLD_SECS) if not self.atv: _LOGGER.error("Unable to send commands, not connected to %s", self.name) @@ -84,5 +88,10 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): raise ValueError("Command not found. Exiting sequence") _LOGGER.info("Sending command %s", single_command) - await attr_value() + + if hold_secs >= 1: + await attr_value(action=InputAction.Hold) + else: + await attr_value() + await asyncio.sleep(delay) diff --git a/homeassistant/components/aprilaire/climate.py b/homeassistant/components/aprilaire/climate.py index 96c1e1ac981..2876d621aef 100644 --- a/homeassistant/components/aprilaire/climate.py +++ b/homeassistant/components/aprilaire/climate.py @@ -107,9 +107,7 @@ class AprilaireClimate(BaseAprilaireEntity, ClimateEntity): features = features | ClimateEntityFeature.PRESET_MODE - features = features | ClimateEntityFeature.FAN_MODE - - return features + return features | ClimateEntityFeature.FAN_MODE @property def current_humidity(self) -> int | None: diff --git a/homeassistant/components/aprilaire/config_flow.py b/homeassistant/components/aprilaire/config_flow.py index 14437e5f3f2..4acc1b9dd9e 100644 --- a/homeassistant/components/aprilaire/config_flow.py +++ b/homeassistant/components/aprilaire/config_flow.py @@ -8,7 +8,7 @@ from typing import Any from pyaprilaire.const import Attribute import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -26,14 +26,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AprilaireConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aprilaire.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: diff --git a/homeassistant/components/aprilaire/coordinator.py b/homeassistant/components/aprilaire/coordinator.py index 7a67dee46a8..7674ff070a6 100644 --- a/homeassistant/components/aprilaire/coordinator.py +++ b/homeassistant/components/aprilaire/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import logging -from typing import Any, Optional +from typing import Any import pyaprilaire.client from pyaprilaire.const import MODELS, Attribute, FunctionalDomain @@ -155,7 +155,7 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol): return self.create_device_name(self.data) - def create_device_name(self, data: Optional[dict[str, Any]]) -> str: + def create_device_name(self, data: dict[str, Any] | None) -> str: """Create the name of the thermostat.""" name = data.get(Attribute.NAME) if data else None diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index a3dac880746..63826f5a385 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/aprs", "iot_class": "cloud_push", "loggers": ["aprslib", "geographiclib", "geopy"], - "requirements": ["aprslib==0.7.0", "geopy==2.3.0"] + "requirements": ["aprslib==0.7.2", "geopy==2.3.0"] } diff --git a/homeassistant/components/aranet/strings.json b/homeassistant/components/aranet/strings.json index 918cfc1d384..ac8d1907770 100644 --- a/homeassistant/components/aranet/strings.json +++ b/homeassistant/components/aranet/strings.json @@ -11,7 +11,7 @@ "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" } }, - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "error": { "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index ac8d389304b..ca08a2b4d16 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -257,7 +257,7 @@ class ArcamFmj(MediaPlayerEntity): for preset in presets.values() ] - root = BrowseMedia( + return BrowseMedia( title="Arcam FMJ Receiver", media_class=MediaClass.DIRECTORY, media_content_id="root", @@ -267,8 +267,6 @@ class ArcamFmj(MediaPlayerEntity): children=radio, ) - return root - @convert_exception async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index f9485636365..3975109e07a 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -2,6 +2,7 @@ from __future__ import annotations +from aiohttp.client_exceptions import ClientResponseError from arris_tg2492lg import ConnectBox, Device import voluptuous as vol @@ -12,6 +13,7 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -25,12 +27,21 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> ArrisDeviceScanner: - """Return the Arris device scanner.""" +async def async_get_scanner( + hass: HomeAssistant, config: ConfigType +) -> ArrisDeviceScanner | None: + """Return the Arris device scanner if successful.""" conf = config[DOMAIN] url = f"http://{conf[CONF_HOST]}" - connect_box = ConnectBox(url, conf[CONF_PASSWORD]) - return ArrisDeviceScanner(connect_box) + websession = async_get_clientsession(hass) + connect_box = ConnectBox(websession, url, conf[CONF_PASSWORD]) + + try: + await connect_box.async_login() + + return ArrisDeviceScanner(connect_box) + except ClientResponseError: + return None class ArrisDeviceScanner(DeviceScanner): @@ -41,23 +52,22 @@ class ArrisDeviceScanner(DeviceScanner): self.connect_box = connect_box self.last_results: list[Device] = [] - def scan_devices(self) -> list[str]: + async def async_scan_devices(self) -> list[str]: """Scan for new devices and return a list with found device IDs.""" - self._update_info() + await self._async_update_info() return [device.mac for device in self.last_results if device.mac] - def get_device_name(self, device: str) -> str | None: + async def async_get_device_name(self, device: str) -> str | None: """Return the name of the given device or None if we don't know.""" - name = next( + return next( (result.hostname for result in self.last_results if result.mac == device), None, ) - return name - def _update_info(self) -> None: + async def _async_update_info(self) -> None: """Ensure the information from the Arris TG2492LG router is up to date.""" - result = self.connect_box.get_connected_devices() + result = await self.connect_box.async_get_connected_devices() last_results: list[Device] = [] mac_addresses: set[str | None] = set() diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json index 0134ea9077d..fa7673b4276 100644 --- a/homeassistant/components/arris_tg2492lg/manifest.json +++ b/homeassistant/components/arris_tg2492lg/manifest.json @@ -2,8 +2,10 @@ "domain": "arris_tg2492lg", "name": "Arris TG2492LG", "codeowners": ["@vanbalken"], + "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["arris_tg2492lg"], - "requirements": ["arris-tg2492lg==1.2.1"] + "requirements": ["arris-tg2492lg==2.2.0"] } diff --git a/homeassistant/components/arve/__init__.py b/homeassistant/components/arve/__init__.py new file mode 100644 index 00000000000..91e38da4c60 --- /dev/null +++ b/homeassistant/components/arve/__init__.py @@ -0,0 +1,34 @@ +"""The Arve integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ArveCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Arve from a config entry.""" + + coordinator = ArveCoordinator(hass) + + 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 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/arve/config_flow.py b/homeassistant/components/arve/config_flow.py new file mode 100644 index 00000000000..23d344d2325 --- /dev/null +++ b/homeassistant/components/arve/config_flow.py @@ -0,0 +1,53 @@ +"""Config flow for Arve integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from asyncarve import Arve, ArveConnectionError, ArveCustomer +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ArveConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Arve.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + + errors: dict[str, str] = {} + if user_input is not None: + arve = Arve( + user_input[CONF_ACCESS_TOKEN], + user_input[CONF_CLIENT_SECRET], + ) + try: + customer: ArveCustomer = await arve.get_customer_id() + except ArveConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(customer.customerId) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Arve", + data=user_input, + ) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ACCESS_TOKEN): str, + vol.Required(CONF_CLIENT_SECRET): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/arve/const.py b/homeassistant/components/arve/const.py new file mode 100644 index 00000000000..1350640f887 --- /dev/null +++ b/homeassistant/components/arve/const.py @@ -0,0 +1,7 @@ +"""Constants for the Arve integration.""" + +import logging + +DOMAIN = "arve" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/arve/coordinator.py b/homeassistant/components/arve/coordinator.py new file mode 100644 index 00000000000..b053e30336b --- /dev/null +++ b/homeassistant/components/arve/coordinator.py @@ -0,0 +1,63 @@ +"""Coordinator for the Arve integration.""" + +from __future__ import annotations + +from datetime import timedelta + +from asyncarve import ( + Arve, + ArveConnectionError, + ArveDeviceInfo, + ArveDevices, + ArveError, + ArveSensProData, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + + +class ArveCoordinator(DataUpdateCoordinator[ArveSensProData]): + """Arve coordinator.""" + + config_entry: ConfigEntry + devices: ArveDevices + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize Arve coordinator.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + ) + + self.arve = Arve( + self.config_entry.data[CONF_ACCESS_TOKEN], + self.config_entry.data[CONF_CLIENT_SECRET], + session=async_get_clientsession(hass), + ) + + async def _async_update_data(self) -> dict[str, ArveDeviceInfo]: + """Fetch data from API endpoint.""" + try: + self.devices = await self.arve.get_devices() + + response_data = { + sn: ArveDeviceInfo( + await self.arve.device_sensor_data(sn), + await self.arve.get_sensor_info(sn), + ) + for sn in self.devices.sn + } + except ArveConnectionError as err: + raise UpdateFailed("Unable to connect to the Arve device") from err + except ArveError as err: + raise UpdateFailed("Unknown error occurred") from err + + return response_data diff --git a/homeassistant/components/arve/entity.py b/homeassistant/components/arve/entity.py new file mode 100644 index 00000000000..46c6bfc75ec --- /dev/null +++ b/homeassistant/components/arve/entity.py @@ -0,0 +1,53 @@ +"""Arve base entity.""" + +from __future__ import annotations + +from asyncarve import ArveDeviceInfo + +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 ArveCoordinator + + +class ArveDeviceEntity(CoordinatorEntity[ArveCoordinator]): + """Defines a base Arve device entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ArveCoordinator, + description: EntityDescription, + serial_number: str, + ) -> None: + """Initialize the Arve device entity.""" + super().__init__(coordinator) + + self.device_serial_number = serial_number + + self.entity_description = description + + self._attr_unique_id = f"{serial_number}_{description.key}" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_number)}, + manufacturer="Calanda Air AG", + model="Arve Sens Pro", + serial_number=serial_number, + name=self.device.info.name, + ) + + @property + def available(self) -> bool: + """Check if device is available.""" + return super()._attr_available and ( + self.device_serial_number in self.coordinator.data + ) + + @property + def device(self) -> ArveDeviceInfo: + """Returns device instance.""" + return self.coordinator.data[self.device_serial_number] diff --git a/homeassistant/components/arve/icons.json b/homeassistant/components/arve/icons.json new file mode 100644 index 00000000000..887a0694e5d --- /dev/null +++ b/homeassistant/components/arve/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "tvoc": { + "default": "mdi:flask" + } + } + } +} diff --git a/homeassistant/components/arve/manifest.json b/homeassistant/components/arve/manifest.json new file mode 100644 index 00000000000..fa33b3309ce --- /dev/null +++ b/homeassistant/components/arve/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "arve", + "name": "Arve", + "codeowners": ["@ikalnyi"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/arve", + "iot_class": "cloud_polling", + "requirements": ["asyncarve==0.0.9"] +} diff --git a/homeassistant/components/arve/sensor.py b/homeassistant/components/arve/sensor.py new file mode 100644 index 00000000000..f95b26b0451 --- /dev/null +++ b/homeassistant/components/arve/sensor.py @@ -0,0 +1,108 @@ +"""Sensor platform for Arve devices.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from asyncarve import ArveSensProData + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import ArveCoordinator +from .entity import ArveDeviceEntity + + +@dataclass(frozen=True, kw_only=True) +class ArveDeviceEntityDescription(SensorEntityDescription): + """Describes Arve device entity.""" + + value_fn: Callable[[ArveSensProData], float | int] + + +SENSORS: tuple[ArveDeviceEntityDescription, ...] = ( + ArveDeviceEntityDescription( + key="CO2", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + value_fn=lambda arve_data: arve_data.co2, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="AQI", + device_class=SensorDeviceClass.AQI, + value_fn=lambda arve_data: arve_data.aqi, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + value_fn=lambda arve_data: arve_data.humidity, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="PM10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + value_fn=lambda arve_data: arve_data.pm10, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="PM25", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + value_fn=lambda arve_data: arve_data.pm25, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda arve_data: arve_data.temperature, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="TVOC", + translation_key="tvoc", + value_fn=lambda arve_data: arve_data.tvoc, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Arve device based on a config entry.""" + coordinator: ArveCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + ArveDevice(coordinator, description, sn) + for description in SENSORS + for sn in coordinator.devices.sn + ) + + +class ArveDevice(ArveDeviceEntity, SensorEntity): + """Define an Arve device.""" + + entity_description: ArveDeviceEntityDescription + + @property + def native_value(self) -> int | float: + """State of the sensor.""" + return self.entity_description.value_fn(self.device.sensors) diff --git a/homeassistant/components/arve/strings.json b/homeassistant/components/arve/strings.json new file mode 100644 index 00000000000..cbfe3c6b065 --- /dev/null +++ b/homeassistant/components/arve/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up your Arve device", + "data": { + "access_token": "Arve token", + "client_secret": "Arve customer token" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "tvoc": { + "name": "Total volatile organic compounds" + } + } + } +} diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index dbbdff38200..79953565769 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -42,6 +42,7 @@ UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( ), AsekoBinarySensorEntityDescription( key="has_error", + translation_key="error", value_fn=lambda unit: unit.has_error, device_class=BinarySensorDeviceClass.PROBLEM, ), @@ -78,7 +79,6 @@ class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity): """Initialize the unit binary sensor.""" super().__init__(unit, coordinator) self.entity_description = entity_description - self._attr_name = f"{self._device_name} {entity_description.name}" self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}" @property diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index f15657d5a91..f481411e551 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -31,6 +31,8 @@ from .pipeline import ( async_create_default_pipeline, async_get_pipeline, async_get_pipelines, + async_migrate_engine, + async_run_migrations, async_setup_pipeline_store, async_update_pipeline, ) @@ -40,6 +42,7 @@ __all__ = ( "DOMAIN", "async_create_default_pipeline", "async_get_pipelines", + "async_migrate_engine", "async_setup", "async_pipeline_from_audio_stream", "async_update_pipeline", @@ -72,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DATA_LAST_WAKE_UP] = {} await async_setup_pipeline_store(hass) + await async_run_migrations(hass) async_register_websocket_api(hass) return True diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 3463d94fb84..36b72dad69c 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -3,6 +3,7 @@ DOMAIN = "assist_pipeline" DATA_CONFIG = f"{DOMAIN}.config" +DATA_MIGRATIONS = f"{DOMAIN}_migrations" DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 01a12b3635b..2251167466c 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -13,7 +13,7 @@ from pathlib import Path from queue import Empty, Queue from threading import Thread import time -from typing import TYPE_CHECKING, Any, Final, cast +from typing import TYPE_CHECKING, Any, Final, Literal, cast import wave import voluptuous as vol @@ -56,6 +56,7 @@ from .const import ( CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, + DATA_MIGRATIONS, DOMAIN, WAKE_WORD_COOLDOWN, ) @@ -583,7 +584,7 @@ class PipelineRun: self.audio_settings.noise_suppression_level, ) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Compare pipeline runs by id.""" if isinstance(other, PipelineRun): return self.id == other.id @@ -1814,3 +1815,47 @@ async def async_setup_pipeline_store(hass: HomeAssistant) -> PipelineData: PIPELINE_FIELDS, ).async_setup(hass) return PipelineData(pipeline_store) + + +@callback +def async_migrate_engine( + hass: HomeAssistant, + engine_type: Literal["conversation", "stt", "tts", "wake_word"], + old_value: str, + new_value: str, +) -> None: + """Register a migration of an engine used in pipelines.""" + hass.data.setdefault(DATA_MIGRATIONS, {})[engine_type] = (old_value, new_value) + + # Run migrations when config is already loaded + if DATA_CONFIG in hass.data: + hass.async_create_background_task( + async_run_migrations(hass), "assist_pipeline_migration", eager_start=True + ) + + +async def async_run_migrations(hass: HomeAssistant) -> None: + """Run pipeline migrations.""" + if not (migrations := hass.data.get(DATA_MIGRATIONS)): + return + + engine_attr = { + "conversation": "conversation_engine", + "stt": "stt_engine", + "tts": "tts_engine", + "wake_word": "wake_word_entity", + } + + updates = [] + + for pipeline in async_get_pipelines(hass): + attr_updates = {} + for engine_type, (old_value, new_value) in migrations.items(): + if getattr(pipeline, engine_attr[engine_type]) == old_value: + attr_updates[engine_attr[engine_type]] = new_value + + if attr_updates: + updates.append((pipeline, attr_updates)) + + for pipeline, attr_updates in updates: + await async_update_pipeline(hass, pipeline, **attr_updates) diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 7550f860a9b..3e8cdf6fa42 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -291,8 +291,11 @@ def websocket_list_runs( msg["id"], { "pipeline_runs": [ - {"pipeline_run_id": id, "timestamp": pipeline_run.timestamp} - for id, pipeline_run in pipeline_debug.items() + { + "pipeline_run_id": pipeline_run_id, + "timestamp": pipeline_run.timestamp, + } + for pipeline_run_id, pipeline_run in pipeline_debug.items() ] }, ) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index 35f3a98251f..c177fb1bb20 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -85,10 +85,10 @@ def handle_errors_and_zip( return data if isinstance(data, dict): - return dict(zip(keys, list(data.values()))) + return dict(zip(keys, list(data.values()), strict=False)) if not isinstance(data, (list, tuple)): raise UpdateFailed("Received invalid data type") - return dict(zip(keys, data)) + return dict(zip(keys, data, strict=False)) return _wrapper @@ -254,7 +254,7 @@ class AsusWrtLegacyBridge(AsusWrtBridge): async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]: """Return a dictionary of available sensors for this bridge.""" sensors_temperatures = await self._get_available_temperature_sensors() - sensors_types = { + return { SENSORS_TYPE_BYTES: { KEY_SENSORS: SENSORS_BYTES, KEY_METHOD: self._get_bytes, @@ -272,7 +272,6 @@ class AsusWrtLegacyBridge(AsusWrtBridge): KEY_METHOD: self._get_temperatures, }, } - return sensors_types async def _get_available_temperature_sensors(self) -> list[str]: """Check which temperature information is available on the router.""" @@ -351,7 +350,7 @@ class AsusWrtHttpBridge(AsusWrtBridge): """Return a dictionary of available sensors for this bridge.""" sensors_temperatures = await self._get_available_temperature_sensors() sensors_loadavg = await self._get_loadavg_sensors_availability() - sensors_types = { + return { SENSORS_TYPE_BYTES: { KEY_SENSORS: SENSORS_BYTES, KEY_METHOD: self._get_bytes, @@ -369,7 +368,6 @@ class AsusWrtHttpBridge(AsusWrtBridge): KEY_METHOD: self._get_temperatures, }, } - return sensors_types async def _get_available_temperature_sensors(self) -> list[str]: """Check which temperature information is available on the router.""" diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e711edd6893..a6b549b8c89 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -135,7 +135,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): self._attr_is_jammed = self._lock_status is LockStatus.JAMMED self._attr_is_locking = self._lock_status is LockStatus.LOCKING - self._attr_is_unlocking = self._lock_status is LockStatus.UNLOCKING + self._attr_is_unlocking = self._lock_status in ( + LockStatus.UNLOCKING, + LockStatus.UNLATCHING, + ) self._attr_extra_state_attributes = { ATTR_BATTERY_LEVEL: self._detail.battery_level diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 27c5f11ec6e..e380a00cbc0 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==2.0.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==3.0.1", "yalexs-ble==2.4.2"] } diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 9332080d9ad..bec8e2f0b97 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -47,7 +47,9 @@ class AugustSubscriberMixin: @callback def _async_scheduled_refresh(self, now: datetime) -> None: """Call the refresh method.""" - self._hass.async_create_task(self._async_refresh(now), eager_start=True) + self._hass.async_create_background_task( + self._async_refresh(now), name=f"{self} schedule refresh", eager_start=True + ) @callback def _async_cancel_update_interval(self, _: Event | None = None) -> None: @@ -71,7 +73,6 @@ class AugustSubscriberMixin: self._stop_interval = self._hass.bus.async_listen( EVENT_HOMEASSISTANT_STOP, self._async_cancel_update_interval, - run_immediately=True, ) @callback diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index 3f635595258..a1e046f302f 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -46,9 +46,9 @@ def validate_and_connect( ret[ATTR_MODEL] = f"{client.version()} ({client.pn()})" ret[ATTR_FIRMWARE] = client.firmware(1) _LOGGER.info("Returning device info=%s", ret) - except AuroraError as err: + except AuroraError: _LOGGER.warning("Could not connect to device=%s", comport) - raise err + raise finally: if client.serline.isOpen(): client.close() diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d0e605e7c1e..3d825cd99b5 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -162,6 +162,7 @@ from homeassistant.util import dt as dt_util from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" +STRICT_CONNECTION_URL = "/auth/strict_connection/temp_token" StoreResultType = Callable[[str, Credentials], str] RetrieveResultType = Callable[[str, str], Credentials | None] @@ -187,6 +188,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(RevokeTokenView()) hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(OAuth2AuthorizeCallbackView()) + hass.http.register_view(StrictConnectionTempTokenView()) websocket_api.async_register_command(hass, websocket_current_user) websocket_api.async_register_command(hass, websocket_create_long_lived_access_token) @@ -195,8 +197,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_delete_all_refresh_tokens) websocket_api.async_register_command(hass, websocket_sign_path) - await login_flow.async_setup(hass, store_result) - await mfa_setup_flow.async_setup(hass) + login_flow.async_setup(hass, store_result) + mfa_setup_flow.async_setup(hass) return True @@ -260,10 +262,10 @@ class TokenView(HomeAssistantView): return await RevokeTokenView.post(self, request) # type: ignore[arg-type] if grant_type == "authorization_code": - return await self._async_handle_auth_code(hass, data, request.remote) + return await self._async_handle_auth_code(hass, data, request) if grant_type == "refresh_token": - return await self._async_handle_refresh_token(hass, data, request.remote) + return await self._async_handle_refresh_token(hass, data, request) return self.json( {"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST @@ -273,7 +275,7 @@ class TokenView(HomeAssistantView): self, hass: HomeAssistant, data: MultiDictProxy[str], - remote_addr: str | None, + request: web.Request, ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") @@ -313,7 +315,7 @@ class TokenView(HomeAssistantView): ) try: access_token = hass.auth.async_create_access_token( - refresh_token, remote_addr + refresh_token, request.remote ) except InvalidAuthError as exc: return self.json( @@ -321,6 +323,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.FORBIDDEN, ) + await hass.auth.session.async_create_session(request, refresh_token) return self.json( { "access_token": access_token, @@ -341,9 +344,9 @@ class TokenView(HomeAssistantView): self, hass: HomeAssistant, data: MultiDictProxy[str], - remote_addr: str | None, + request: web.Request, ) -> web.Response: - """Handle authorization code request.""" + """Handle refresh token request.""" client_id = data.get("client_id") if client_id is not None and not indieauth.verify_client_id(client_id): return self.json( @@ -381,7 +384,7 @@ class TokenView(HomeAssistantView): try: access_token = hass.auth.async_create_access_token( - refresh_token, remote_addr + refresh_token, request.remote ) except InvalidAuthError as exc: return self.json( @@ -389,6 +392,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.FORBIDDEN, ) + await hass.auth.session.async_create_session(request, refresh_token) return self.json( { "access_token": access_token, @@ -437,6 +441,20 @@ class LinkUserView(HomeAssistantView): return self.json_message("User linked") +class StrictConnectionTempTokenView(HomeAssistantView): + """View to get temporary strict connection token.""" + + url = STRICT_CONNECTION_URL + name = "api:auth:strict_connection:temp_token" + requires_auth = False + + async def get(self, request: web.Request) -> web.Response: + """Get a temporary token and redirect to main page.""" + hass = request.app[KEY_HASS] + await hass.auth.session.async_create_temp_unauthorized_session(request) + raise web.HTTPSeeOther(location="/") + + @callback def _create_auth_code_store() -> tuple[StoreResultType, RetrieveResultType]: """Create an in memory store.""" diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 232f067b673..45de94d5a70 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -127,9 +127,9 @@ def verify_client_id(client_id: str) -> bool: """Verify that the client id is valid.""" try: _parse_client_id(client_id) - return True except ValueError: return False + return True def _parse_url(url: str) -> ParseResult: diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 6c33d270f5f..5bad0dbb999 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -91,7 +91,7 @@ from homeassistant.components.http.ban import ( ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.network import is_cloud_connection from homeassistant.util.network import is_local @@ -105,7 +105,8 @@ if TYPE_CHECKING: from . import StoreResultType -async def async_setup( +@callback +def async_setup( hass: HomeAssistant, store_result: Callable[[str, Credentials], str] ) -> None: """Component to allow users to login.""" diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index aee08186267..35d87cafd4f 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -62,7 +62,8 @@ class MfaFlowManager(data_entry_flow.FlowManager): return result -async def async_setup(hass: HomeAssistant) -> None: +@callback +def async_setup(hass: HomeAssistant) -> None: """Init mfa setup flow manager.""" hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) @@ -147,8 +148,7 @@ def _prepare_result_json( ) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - data = result.copy() - return data + return result.copy() if result["type"] != data_entry_flow.FlowResultType.FORM: return result diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index fbebc82225f..fa242ac1557 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,9 +6,9 @@ from abc import ABC, abstractmethod import asyncio from collections.abc import Callable, Mapping from dataclasses import dataclass -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import Any, Protocol, cast import voluptuous as vol @@ -112,12 +112,6 @@ from .const import ( from .helpers import async_get_blueprints from .trace import trace_automation -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -337,17 +331,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_get_blueprints(hass).async_reset_cache() if (conf := await component.async_prepare_reload(skip_reset=True)) is None: return - await _async_process_config(hass, conf, component) + if automation_id := service_call.data.get(CONF_ID): + await _async_process_single_config(hass, conf, component, automation_id) + else: + await _async_process_config(hass, conf, component) hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context) - reload_helper = ReloadServiceHelper(reload_service_handler) + def reload_targets(service_call: ServiceCall) -> set[str | None]: + if automation_id := service_call.data.get(CONF_ID): + return {automation_id} + return {automation.unique_id for automation in component.entities} + + reload_helper = ReloadServiceHelper(reload_service_handler, reload_targets) async_register_admin_service( hass, DOMAIN, SERVICE_RELOAD, reload_helper.execute_service, - schema=vol.Schema({}), + schema=vol.Schema({vol.Optional(CONF_ID): str}), ) websocket_api.async_register_command(hass, websocket_config) @@ -426,15 +428,10 @@ class UnavailableAutomationEntity(BaseAutomationEntity): raw_config: ConfigType | None, ) -> None: """Initialize an automation entity.""" - self._name = name + self._attr_name = name self._attr_unique_id = automation_id self.raw_config = raw_config - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @cached_property def referenced_labels(self) -> set[str]: """Return a set of referenced labels.""" @@ -494,7 +491,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): trace_config: ConfigType, ) -> None: """Initialize an automation entity.""" - self._name = name + self._attr_name = name self._trigger_config = trigger_config self._async_detach_triggers: CALLBACK_TYPE | None = None self._cond_func = cond_func @@ -510,11 +507,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): self._trace_config = trace_config self._attr_unique_id = automation_id - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the entity state attributes.""" @@ -620,18 +612,20 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): ) if enable_automation: - await self.async_enable() + await self._async_enable() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on and update the state.""" - await self.async_enable() + await self._async_enable() + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if CONF_STOP_ACTIONS in kwargs: - await self.async_disable(kwargs[CONF_STOP_ACTIONS]) + await self._async_disable(kwargs[CONF_STOP_ACTIONS]) else: - await self.async_disable() + await self._async_disable() + self.async_write_ha_state() async def async_trigger( self, @@ -713,7 +707,10 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): @callback def started_action() -> None: - self.hass.bus.async_fire( + # This is always a callback from a coro so there is no + # risk of this running in a thread which allows us to use + # async_fire_internal + self.hass.bus.async_fire_internal( EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) @@ -738,7 +735,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): translation_placeholders={ "service": f"{err.domain}.{err.service}", "entity_id": self.entity_id, - "name": self.name or self.entity_id, + "name": self._attr_name or self.entity_id, "edit": f"/config/automation/edit/{self.unique_id}", }, ) @@ -759,7 +756,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): async def async_will_remove_from_hass(self) -> None: """Remove listeners when removing automation from Home Assistant.""" await super().async_will_remove_from_hass() - await self.async_disable() + await self._async_disable() async def _async_enable_automation(self, event: Event) -> None: """Start automation on startup.""" @@ -768,32 +765,34 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): return self._async_detach_triggers = await self._async_attach_triggers(True) + self.async_write_ha_state() - async def async_enable(self) -> None: + async def _async_enable(self) -> None: """Enable this automation entity. - This method is a coroutine. + This method is not expected to write state to the + state machine. """ if self._is_enabled: return self._is_enabled = True - # HomeAssistant is starting up if self.hass.state is not CoreState.not_running: self._async_detach_triggers = await self._async_attach_triggers(False) - self.async_write_ha_state() return self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._async_enable_automation, - run_immediately=True, ) - self.async_write_ha_state() - async def async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None: - """Disable the automation entity.""" + async def _async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None: + """Disable the automation entity. + + This method is not expected to write state to the + state machine. + """ if not self._is_enabled and not self.action_script.runs: return @@ -806,8 +805,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): if stop_actions: await self.action_script.async_stop() - self.async_write_ha_state() - def _log_callback(self, level: int, msg: str, **kwargs: Any) -> None: """Log helper callback.""" self._logger.log(level, "%s %s", msg, self.name, **kwargs) @@ -833,7 +830,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): ) -> Callable[[], None] | None: """Set up the triggers.""" this = None - self.async_write_ha_state() if state := self.hass.states.get(self.entity_id): this = state.as_dict() variables = {"this": this} @@ -874,6 +870,7 @@ class AutomationEntityConfig: async def _prepare_automation_config( hass: HomeAssistant, config: ConfigType, + wanted_automation_id: str | None, ) -> list[AutomationEntityConfig]: """Parse configuration and prepare automation entity configuration.""" automation_configs: list[AutomationEntityConfig] = [] @@ -881,6 +878,10 @@ async def _prepare_automation_config( conf: list[ConfigType] = config[DOMAIN] for list_no, config_block in enumerate(conf): + automation_id: str | None = config_block.get(CONF_ID) + if wanted_automation_id is not None and automation_id != wanted_automation_id: + continue + raw_config = cast(AutomationConfig, config_block).raw_config raw_blueprint_inputs = cast(AutomationConfig, config_block).raw_blueprint_inputs validation_failed = cast(AutomationConfig, config_block).validation_failed @@ -1040,7 +1041,7 @@ async def _async_process_config( return automation_matches, config_matches - automation_configs = await _prepare_automation_config(hass, config) + automation_configs = await _prepare_automation_config(hass, config, None) automations: list[BaseAutomationEntity] = list(component.entities) # Find automations and configurations which have matches @@ -1064,6 +1065,41 @@ async def _async_process_config( await component.async_add_entities(entities) +def _automation_matches_config( + automation: BaseAutomationEntity | None, config: AutomationEntityConfig | None +) -> bool: + """Return False if an automation's config has been changed.""" + if not automation: + return False + if not config: + return False + name = _automation_name(config) + return automation.name == name and automation.raw_config == config.raw_config + + +async def _async_process_single_config( + hass: HomeAssistant, + config: dict[str, Any], + component: EntityComponent[BaseAutomationEntity], + automation_id: str, +) -> None: + """Process config and add a single automation.""" + + automation_configs = await _prepare_automation_config(hass, config, automation_id) + automation = next( + (x for x in component.entities if x.unique_id == automation_id), None + ) + automation_config = automation_configs[0] if automation_configs else None + + if _automation_matches_config(automation, automation_config): + return + + if automation: + await automation.async_remove() + entities = await _create_automation_entities(hass, automation_configs) + await component.async_add_entities(entities) + + async def _async_process_if( hass: HomeAssistant, name: str, config: dict[str, Any] ) -> IfAction | None: diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 7b9c8cf5809..33ed586f901 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,5 +1,8 @@ """Describe logbook events.""" +from collections.abc import Callable +from typing import Any + from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, @@ -16,11 +19,16 @@ from .const import DOMAIN @callback -def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore[no-untyped-def] +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[ + [str, str, Callable[[LazyEventPartialState], dict[str, Any]]], None + ], +) -> None: """Describe logbook events.""" @callback - def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore[no-untyped-def] + def async_describe_logbook_event(event: LazyEventPartialState) -> dict[str, Any]: """Describe a logbook event.""" data = event.data message = "triggered" diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 754c062ec2c..e7f671e6f05 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -65,7 +65,7 @@ def trace_automation( except Exception as ex: if automation_id: trace.set_error(ex) - raise ex + raise finally: if automation_id: trace.finished() diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index d3c4703e89c..a6efc3640f9 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -250,13 +250,12 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): try: user = await awair.user() devices = await user.devices() - if not devices: - return (None, "no_devices_found") - - return (user, None) - except AuthError: return (None, "invalid_access_token") except AwairError as err: LOGGER.error("Unexpected API error: %s", err) return (None, "unknown") + + if not devices: + return (None, "no_devices_found") + return (user, None) diff --git a/homeassistant/components/awair/coordinator.py b/homeassistant/components/awair/coordinator.py index 8e554b3b9e0..b63efff7733 100644 --- a/homeassistant/components/awair/coordinator.py +++ b/homeassistant/components/awair/coordinator.py @@ -111,7 +111,7 @@ class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): devices = await self._awair.devices() self._device = devices[0] result = await self._fetch_air_data(self._device) - return {result.device.uuid: result} except AwairError as err: LOGGER.error("Unexpected API error: %s", err) raise UpdateFailed(err) from err + return {result.device.uuid: result} diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index 61caf4c2318..470ccc0e409 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/aws", "iot_class": "cloud_push", "loggers": ["aiobotocore", "botocore"], - "requirements": ["aiobotocore==2.9.1"] + "requirements": ["aiobotocore==2.12.1"] } diff --git a/homeassistant/components/axis/hub/event_source.py b/homeassistant/components/axis/hub/event_source.py new file mode 100644 index 00000000000..7f2bfe7c982 --- /dev/null +++ b/homeassistant/components/axis/hub/event_source.py @@ -0,0 +1,93 @@ +"""Axis network device abstraction.""" + +from __future__ import annotations + +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 HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_when_setup + + +class AxisEventSource: + """Manage connection to event sources from an 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.signal_reachable = f"axis_reachable_{config_entry.entry_id}" + + self.available = True + + @callback + def setup(self) -> None: + """Set up the device events.""" + self.api.stream.connection_status_callback.append(self._connection_status_cb) + 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 teardown(self) -> None: + """Tear down connections.""" + self._disconnect_from_stream() + + @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 _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) + + @callback + def _connection_status_cb(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) diff --git a/homeassistant/components/axis/hub/hub.py b/homeassistant/components/axis/hub/hub.py index 4abd1358417..4e58e3be7c6 100644 --- a/homeassistant/components/axis/hub/hub.py +++ b/homeassistant/components/axis/hub/hub.py @@ -5,24 +5,17 @@ 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 +from .event_source import AxisEventSource class AxisHub: @@ -35,9 +28,9 @@ class AxisHub: self.hass = hass self.config = AxisConfig.from_config_entry(config_entry) self.entity_loader = AxisEntityLoader(self) + self.event_source = AxisEventSource(hass, config_entry, api) 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) @@ -51,32 +44,23 @@ class AxisHub: hub: AxisHub = hass.data[AXIS_DOMAIN][config_entry.entry_id] return hub + @property + def available(self) -> bool: + """Connection state to the device.""" + return self.event_source.available + # 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}" + return self.event_source.signal_reachable @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 @@ -89,6 +73,7 @@ class AxisHub: """ hub = AxisHub.get_hub(hass, config_entry) hub.config = AxisConfig.from_config_entry(config_entry) + hub.event_source.config_entry = config_entry hub.api.config.host = hub.config.host async_dispatcher_send(hass, hub.signal_new_address) @@ -106,57 +91,19 @@ class AxisHub: 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() + self.event_source.setup() async def shutdown(self, event: Event) -> None: """Stop the event stream.""" - self.disconnect_from_stream() + self.event_source.teardown() @callback def teardown(self) -> None: """Reset this device to default state.""" - self.disconnect_from_stream() + self.event_source.teardown() diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index b3898b7aab8..2f057f96286 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -19,6 +19,10 @@ { "hostname": "axis-b8a44f*", "macaddress": "B8A44F*" + }, + { + "hostname": "axis-e82725*", + "macaddress": "E82725*" } ], "documentation": "https://www.home-assistant.io/integrations/axis", @@ -50,6 +54,12 @@ "properties": { "macaddress": "b8a44f*" } + }, + { + "type": "_axis-video._tcp.local.", + "properties": { + "macaddress": "e82725*" + } } ] } diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index e2b761708a5..537019fb9c1 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -43,7 +44,8 @@ class AzureDevOpsEntityDescription(EntityDescription): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" - client = DevOpsClient() + aiohttp_session = async_get_clientsession(hass) + client = DevOpsClient(session=aiohttp_session) if entry.data.get(CONF_PAT) is not None: await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) @@ -62,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from Azure DevOps.""" try: - return await client.get_builds( + builds = await client.get_builds( entry.data[CONF_ORG], entry.data[CONF_PROJECT], BUILDS_QUERY, @@ -70,6 +72,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as exception: raise UpdateFailed from exception + if builds is None: + raise UpdateFailed("No builds found") + + return builds + coordinator = DataUpdateCoordinator( hass, _LOGGER, @@ -111,8 +118,8 @@ class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild """Initialize the Azure DevOps entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_unique_id: str = "_".join( - [entity_description.organization, entity_description.key] + self._attr_unique_id: str = ( + f"{entity_description.organization}_{entity_description.key}" ) self._organization: str = entity_description.organization self._project_name: str = entity_description.project.name diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 336fd2ca8df..ffb0abf609a 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -10,6 +10,7 @@ import aiohttp import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN @@ -56,7 +57,8 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): """Check the setup of the flow.""" errors: dict[str, str] = {} - client = DevOpsClient() + aiohttp_session = async_get_clientsession(self.hass) + client = DevOpsClient(session=aiohttp_session) try: if self._pat is not None: diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index c97d81046da..0d5e5a1c685 100644 --- a/homeassistant/components/azure_devops/manifest.json +++ b/homeassistant/components/azure_devops/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/azure_devops", "iot_class": "cloud_polling", "loggers": ["aioazuredevops"], - "requirements": ["aioazuredevops==1.3.5"] + "requirements": ["aioazuredevops==2.0.0"] } diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index a6e4ee95cad..514db5462e9 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -39,19 +39,23 @@ async def async_setup_entry( AzureDevOpsSensor( coordinator, AzureDevOpsSensorEntityDescription( - key=f"{build.project.id}_{build.definition.id}_latest_build", + key=f"{build.project.project_id}_{build.definition.build_id}_latest_build", translation_key="latest_build", translation_placeholders={"definition_name": build.definition.name}, attrs=lambda build: { - "definition_id": build.definition.id, - "definition_name": build.definition.name, - "id": build.id, + "definition_id": ( + build.definition.build_id if build.definition else None + ), + "definition_name": ( + build.definition.name if build.definition else None + ), + "id": build.build_id, "reason": build.reason, "result": build.result, "source_branch": build.source_branch, "source_version": build.source_version, "status": build.status, - "url": build.links.web, + "url": build.links.web if build.links else None, "queue_time": build.queue_time, "start_time": build.start_time, "finish_time": build.finish_time, diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 0a84ca44141..668444f9990 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -158,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, run_immediately=True + MATCH_ALL, self.async_listen ) self._schedule_next_send() diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 9876d7ffec3..4d5020bdf02 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -9,7 +9,7 @@ QUERY_INTERVAL = 300 RUN_TIMEOUT = 20 -PRESET_MODE_AUTO = "Auto" +PRESET_MODE_AUTO = "auto" SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 15c6519747d..6c90e2a53cb 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -48,6 +48,7 @@ class BAFFan(BAFEntity, FanEntity): _attr_preset_modes = [PRESET_MODE_AUTO] _attr_speed_count = SPEED_COUNT _attr_name = None + _attr_translation_key = "baf" @callback def _async_update_attrs(self) -> None: diff --git a/homeassistant/components/baf/icons.json b/homeassistant/components/baf/icons.json new file mode 100644 index 00000000000..c91c4cde86a --- /dev/null +++ b/homeassistant/components/baf/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "fan": { + "baf": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "mdi:fan-auto" + } + } + } + } + } + } +} diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index 5143b519d27..e2f02a6095e 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -26,6 +26,17 @@ "name": "Auto comfort" } }, + "fan": { + "baf": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]" + } + } + } + } + }, "number": { "comfort_min_speed": { "name": "Auto Comfort Minimum Speed" diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index d6a80e8fa8f..7e220bd46f8 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -18,7 +18,13 @@ from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, Platform.LIGHT] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.SELECT, +] KEEP_ALIVE_INTERVAL = timedelta(minutes=1) diff --git a/homeassistant/components/balboa/icons.json b/homeassistant/components/balboa/icons.json index 7261f71bd00..7454366f692 100644 --- a/homeassistant/components/balboa/icons.json +++ b/homeassistant/components/balboa/icons.json @@ -27,6 +27,11 @@ "off": "mdi:pump-off" } } + }, + "select": { + "temperature_range": { + "default": "mdi:thermometer-lines" + } } } } diff --git a/homeassistant/components/balboa/select.py b/homeassistant/components/balboa/select.py new file mode 100644 index 00000000000..3fdd8c4d014 --- /dev/null +++ b/homeassistant/components/balboa/select.py @@ -0,0 +1,52 @@ +"""Support for Spa Client selects.""" + +from pybalboa import SpaClient, SpaControl +from pybalboa.enums import LowHighRange + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BalboaEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the spa select entity.""" + spa: SpaClient = hass.data[DOMAIN][entry.entry_id] + async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)]) + + +class BalboaTempRangeSelectEntity(BalboaEntity, SelectEntity): + """Representation of a Temperature Range select.""" + + _attr_icon = "mdi:thermometer-lines" + _attr_name = "Temperature range" + _attr_unique_id = "temperature_range" + _attr_translation_key = "temperature_range" + _attr_options = [ + LowHighRange.LOW.name.lower(), + LowHighRange.HIGH.name.lower(), + ] + + def __init__(self, control: SpaControl) -> None: + """Initialise the select.""" + super().__init__(control.client, "TempHiLow") + self._control = control + + @property + def current_option(self) -> str | None: + """Return current select option.""" + if self._control.state == LowHighRange.HIGH: + return LowHighRange.HIGH.name.lower() + return LowHighRange.LOW.name.lower() + + async def async_select_option(self, option: str) -> None: + """Select temperature range high/low mode.""" + if option == LowHighRange.HIGH.name.lower(): + await self._client.set_temperature_range(LowHighRange.HIGH) + else: + await self._client.set_temperature_range(LowHighRange.LOW) diff --git a/homeassistant/components/balboa/strings.json b/homeassistant/components/balboa/strings.json index 3c8f82764d4..6ced7dfd8c3 100644 --- a/homeassistant/components/balboa/strings.json +++ b/homeassistant/components/balboa/strings.json @@ -65,6 +65,15 @@ "only_light": { "name": "Light" } + }, + "select": { + "temperature_range": { + "name": "Temperature range", + "state": { + "low": "Low", + "high": "High" + } + } } } } diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py index 2488c2e64f5..07b9d0befe1 100644 --- a/homeassistant/components/bang_olufsen/__init__.py +++ b/homeassistant/components/bang_olufsen/__init__.py @@ -4,7 +4,11 @@ from __future__ import annotations from dataclasses import dataclass -from aiohttp.client_exceptions import ClientConnectorError +from aiohttp.client_exceptions import ( + ClientConnectorError, + ClientOSError, + ServerTimeoutError, +) from mozart_api.exceptions import ApiException from mozart_api.mozart_client import MozartClient @@ -44,12 +48,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=entry.data[CONF_MODEL], ) - client = MozartClient(host=entry.data[CONF_HOST], websocket_reconnect=True) + client = MozartClient(host=entry.data[CONF_HOST]) - # Check connection and try to initialize it. + # Check API and WebSocket connection try: - await client.get_battery_state(_request_timeout=3) - except (ApiException, ClientConnectorError, TimeoutError) as error: + await client.check_device_connection(True) + except* ( + ClientConnectorError, + ClientOSError, + ServerTimeoutError, + ApiException, + TimeoutError, + ) as error: await client.close_api_client() raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error @@ -61,11 +71,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client, ) - # Check and start WebSocket connection - if not await client.connect_notifications(remote_control=True): - raise ConfigEntryNotReady( - f"Unable to connect to {entry.title} WebSocket notification channel" - ) + # Start WebSocket connection + await client.connect_notifications(remote_control=True, reconnect=True) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/bang_olufsen/manifest.json b/homeassistant/components/bang_olufsen/manifest.json index 3c920a99d7f..f2b31293227 100644 --- a/homeassistant/components/bang_olufsen/manifest.json +++ b/homeassistant/components/bang_olufsen/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bang_olufsen", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mozart-api==3.2.1.150.6"], + "requirements": ["mozart-api==3.4.1.8.5"], "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 935c057efc8..9f55790d711 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -363,7 +363,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" if self._volume.muted and self._volume.muted.muted: - return self._volume.muted.muted + # The any return here is side effect of pydantic v2 compatibility + # This will be fixed in the future. + return self._volume.muted.muted # type: ignore[no-any-return] return None @property diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index b6298040b6b..470732f36d2 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -28,13 +28,12 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import ConditionError, TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, TrackTemplateResultInfo, diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 4fd99c309bc..dad398e2525 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Literal, final +from typing import Literal, final import voluptuous as vol @@ -28,11 +28,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 318bb18772a..7461d7b2a2b 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Mapping -import contextlib import logging from typing import Any @@ -97,7 +96,10 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): await self._camera.async_arm(True) except TimeoutError as er: - raise HomeAssistantError("Blink failed to arm camera") from er + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_arm", + ) from er self._camera.motion_enabled = True await self.coordinator.async_refresh() @@ -107,7 +109,10 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): try: await self._camera.async_arm(False) except TimeoutError as er: - raise HomeAssistantError("Blink failed to disarm camera") from er + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_disarm", + ) from er self._camera.motion_enabled = False await self.coordinator.async_refresh() @@ -124,8 +129,14 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): async def trigger_camera(self) -> None: """Trigger camera to take a snapshot.""" - with contextlib.suppress(TimeoutError): + try: await self._camera.snap_picture() + except TimeoutError as er: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_snap", + ) from er + self.async_write_ha_state() def camera_image( diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 09bbba4c226..2c0be3d972c 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -106,16 +106,31 @@ }, "exceptions": { "integration_not_found": { - "message": "Integration '{target}' not found in registry" + "message": "Integration \"{target}\" not found in registry." }, "no_path": { "message": "Can't write to directory {target}, no access to path!" }, "cant_write": { - "message": "Can't write to file" + "message": "Can't write to file." }, "not_loaded": { - "message": "{target} is not loaded" + "message": "{target} is not loaded." + }, + "failed_arm": { + "message": "Blink failed to arm camera." + }, + "failed_disarm": { + "message": "Blink failed to disarm camera." + }, + "failed_snap": { + "message": "Blink failed to snap a picture." + }, + "failed_arm_motion": { + "message": "Blink failed to arm camera motion detection." + }, + "failed_disarm_motion": { + "message": "Blink failed to disarm camera motion detection." } }, "issues": { diff --git a/homeassistant/components/blink/switch.py b/homeassistant/components/blink/switch.py index 1bfd257ecbe..ab9b825ded1 100644 --- a/homeassistant/components/blink/switch.py +++ b/homeassistant/components/blink/switch.py @@ -75,7 +75,8 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity): except TimeoutError as er: raise HomeAssistantError( - "Blink failed to arm camera motion detection" + translation_domain=DOMAIN, + translation_key="failed_arm_motion", ) from er await self.coordinator.async_refresh() @@ -87,7 +88,8 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity): except TimeoutError as er: raise HomeAssistantError( - "Blink failed to dis-arm camera motion detection" + translation_domain=DOMAIN, + translation_key="failed_disarm_motion", ) from er await self.coordinator.async_refresh() diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py index 4024b8b3326..f8529a4103b 100644 --- a/homeassistant/components/bluemaestro/sensor.py +++ b/homeassistant/components/bluemaestro/sensor.py @@ -75,6 +75,7 @@ SENSOR_DESCRIPTIONS = { ): SensorEntityDescription( key=f"{BlueMaestroSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, + translation_key="dew_point", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -110,10 +111,7 @@ def sensor_update_to_bluetooth_data_update( device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, - entity_names={ - device_key_to_bluetooth_entity_key(device_key): sensor_values.name - for device_key, sensor_values in sensor_update.entity_values.items() - }, + entity_names={}, ) diff --git a/homeassistant/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json index d1d544c2381..8f84456d3a7 100644 --- a/homeassistant/components/bluemaestro/strings.json +++ b/homeassistant/components/bluemaestro/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", @@ -18,5 +18,12 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "dew_point": { + "name": "Dew point" + } + } } } diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 9377557d025..6c63067a1c1 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -863,8 +863,6 @@ class BluesoundPlayer(MediaPlayerEntity): if self._group_name is None: return None - bluesound_group = [] - device_group = self._group_name.split("+") sorted_entities = sorted( @@ -872,14 +870,12 @@ class BluesoundPlayer(MediaPlayerEntity): key=lambda entity: entity.is_master, reverse=True, ) - bluesound_group = [ + return [ entity.name for entity in sorted_entities if entity.bluesound_device_name in device_group ] - return bluesound_group - async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: @@ -938,7 +934,7 @@ class BluesoundPlayer(MediaPlayerEntity): selected_source = items[0] url = f"Play?url={selected_source['url']}&preset_id&image={selected_source['image']}" - if "is_raw_url" in selected_source and selected_source["is_raw_url"]: + if selected_source.get("is_raw_url"): url = selected_source["url"] return await self.send_bluesound_command(url) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1e091ec32cc..acc38cad58b 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -53,7 +53,6 @@ from homeassistant.loader import async_get_bluetooth from . import models, passive_update_processor from .api import ( - _get_manager, async_address_present, async_ble_device_from_address, async_discovered_service_info, @@ -87,6 +86,7 @@ from .manager import HomeAssistantBluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import BluetoothCallback, BluetoothChange from .storage import BluetoothStorage +from .util import adapter_title if TYPE_CHECKING: from homeassistant.helpers.typing import ConfigType @@ -130,13 +130,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -async def _async_get_adapter_from_address( - hass: HomeAssistant, address: str -) -> str | None: - """Get an adapter by the address.""" - return await _get_manager(hass).async_get_adapter_from_address(address) - - async def _async_start_adapter_discovery( hass: HomeAssistant, manager: HomeAssistantBluetoothManager, @@ -159,6 +152,7 @@ async def _async_start_adapter_discovery( cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, immediate=False, function=_async_rediscover_adapters, + background=True, ) @hass_callback @@ -166,9 +160,7 @@ async def _async_start_adapter_discovery( """Shutdown debouncer.""" discovery_debouncer.async_shutdown() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer) async def _async_call_debouncer(now: datetime.datetime) -> None: """Call the debouncer at a later time.""" @@ -201,12 +193,22 @@ async def _async_start_adapter_discovery( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()), - run_immediately=True, ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" + if platform.system() == "Linux": + # Remove any config entries that are using the default address + # that were created from discovering adapters in a crashed state + # + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed + for entry in list(hass.config_entries.async_entries(DOMAIN)): + if entry.unique_id == DEFAULT_ADDRESS: + await hass.config_entries.async_remove(entry.entry_id) + bluetooth_adapters = get_adapters() bluetooth_storage = BluetoothStorage(hass) slot_manager = BleakSlotManager() @@ -268,13 +270,19 @@ async def async_discover_adapters( adapters: dict[str, AdapterDetails], ) -> None: """Discover adapters and start flows.""" - if platform.system() == "Windows": + system = platform.system() + if system == "Windows": # We currently do not have a good way to detect if a bluetooth device is # available on Windows. We will just assume that it is not unless they # actively add it. return for adapter, details in adapters.items(): + if system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS: + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed so we should not try to start a flow for it. + continue discovery_flow.async_create_flow( hass, DOMAIN, @@ -306,35 +314,36 @@ async def async_update_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for a bluetooth scanner.""" + manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] address = entry.unique_id assert address is not None - adapter = await _async_get_adapter_from_address(hass, address) + adapter = await manager.async_get_adapter_from_address_or_recover(address) if adapter is None: raise ConfigEntryNotReady( f"Bluetooth adapter {adapter} with address {address} not found" ) - passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE - manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] scanner = HaScanner(mode, adapter, address) + scanner.async_setup() try: - scanner.async_setup() - except RuntimeError as err: + await scanner.async_start() + except (RuntimeError, ScannerStartError) as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - try: - await scanner.async_start() - except ScannerStartError as err: - raise ConfigEntryNotReady from err adapters = await manager.async_get_bluetooth_adapters() details = adapters[adapter] + if entry.title == address: + hass.config_entries.async_update_entry( + entry, title=adapter_title(adapter, details) + ) slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots)) await async_update_device(hass, entry, adapter, details) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner entry.async_on_unload(entry.add_update_listener(async_update_listener)) + entry.async_on_unload(scanner.async_stop) return True diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 2b5980fbcd6..90d2624fb0f 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -2,13 +2,16 @@ from __future__ import annotations +import platform from typing import Any, cast from bluetooth_adapters import ( ADAPTER_ADDRESS, + ADAPTER_MANUFACTURER, + DEFAULT_ADDRESS, AdapterDetails, adapter_human_name, - adapter_unique_name, + adapter_model, get_adapters, ) import voluptuous as vol @@ -24,6 +27,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import models from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN +from .util import adapter_title OPTIONS_SCHEMA = vol.Schema( { @@ -35,6 +39,14 @@ OPTIONS_FLOW = { } +def adapter_display_info(adapter: str, details: AdapterDetails) -> str: + """Return the adapter display info.""" + name = adapter_human_name(adapter, details[ADAPTER_ADDRESS]) + model = adapter_model(details) + manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown" + return f"{name} {manufacturer} {model}" + + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" @@ -45,6 +57,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): self._adapter: str | None = None self._details: AdapterDetails | None = None self._adapters: dict[str, AdapterDetails] = {} + self._placeholders: dict[str, str] = {} async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType @@ -54,11 +67,23 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS]) await self.async_set_unique_id(self._details[ADAPTER_ADDRESS]) self._abort_if_unique_id_configured() - self.context["title_placeholders"] = { - "name": adapter_human_name(self._adapter, self._details[ADAPTER_ADDRESS]) - } + details = self._details + self._async_set_adapter_info(self._adapter, details) return await self.async_step_single_adapter() + @callback + def _async_set_adapter_info(self, adapter: str, details: AdapterDetails) -> None: + """Set the adapter info.""" + name = adapter_human_name(adapter, details[ADAPTER_ADDRESS]) + model = adapter_model(details) + manufacturer = details[ADAPTER_MANUFACTURER] + self._placeholders = { + "name": name, + "model": model, + "manufacturer": manufacturer or "Unknown", + } + self.context["title_placeholders"] = self._placeholders + async def async_step_single_adapter( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -67,6 +92,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): details = self._details assert adapter is not None assert details is not None + assert self._placeholders is not None address = details[ADAPTER_ADDRESS] @@ -74,12 +100,12 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( - title=adapter_unique_name(adapter, address), data={} + title=adapter_title(adapter, details), data={} ) return self.async_show_form( step_id="single_adapter", - description_placeholders={"name": adapter_human_name(adapter, address)}, + description_placeholders=self._placeholders, ) async def async_step_multiple_adapters( @@ -89,21 +115,27 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: assert self._adapters is not None adapter = user_input[CONF_ADAPTER] - address = self._adapters[adapter][ADAPTER_ADDRESS] + details = self._adapters[adapter] + address = details[ADAPTER_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( - title=adapter_unique_name(adapter, address), data={} + title=adapter_title(adapter, details), data={} ) configured_addresses = self._async_current_ids() bluetooth_adapters = get_adapters() await bluetooth_adapters.refresh() self._adapters = bluetooth_adapters.adapters + system = platform.system() unconfigured_adapters = [ adapter for adapter, details in self._adapters.items() if details[ADAPTER_ADDRESS] not in configured_addresses + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed + and not (system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS) ] if not unconfigured_adapters: ignored_adapters = len( @@ -116,6 +148,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): if len(unconfigured_adapters) == 1: self._adapter = list(self._adapters)[0] self._details = self._adapters[self._adapter] + self._async_set_adapter_info(self._adapter, self._details) return await self.async_step_single_adapter() return self.async_show_form( @@ -124,8 +157,8 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): { vol.Required(CONF_ADAPTER): vol.In( { - adapter: adapter_human_name( - adapter, self._adapters[adapter][ADAPTER_ADDRESS] + adapter: adapter_display_info( + adapter, self._adapters[adapter] ) for adapter in sorted(unconfigured_adapters) } diff --git a/homeassistant/components/bluetooth/diagnostics.py b/homeassistant/components/bluetooth/diagnostics.py index a45500265cf..1c9c9a56b2e 100644 --- a/homeassistant/components/bluetooth/diagnostics.py +++ b/homeassistant/components/bluetooth/diagnostics.py @@ -10,7 +10,7 @@ from bluetooth_adapters import get_dbus_managed_objects from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import _get_manager +from .api import _get_manager async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 3a240e9f01e..2eb07c5133f 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -135,11 +135,9 @@ class HomeAssistantBluetoothManager(BluetoothManager): self._bluetooth_adapters, self.storage ) self._cancel_logging_listener = self.hass.bus.async_listen( - 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 + EVENT_LOGGING_CHANGED, self._async_logging_changed ) + 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 58009216464..4bb84ab6dc3 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,10 +16,10 @@ "requirements": [ "bleak==0.21.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.18.0", - "bluetooth-auto-recovery==1.4.0", + "bluetooth-adapters==0.19.1", + "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.4.2" + "habluetooth==2.8.0" ] } diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index b7a7a165f71..87f7c7a9b20 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -116,11 +116,10 @@ def deserialize_entity_description( def serialize_entity_description(description: EntityDescription) -> dict[str, Any]: """Serialize an entity description.""" - as_dict = dataclasses.asdict(description) return { - field.name: as_dict[field.name] + field.name: value for field in cached_fields(type(description)) - if field.default != as_dict.get(field.name) + if (value := getattr(description, field.name)) != field.default } @@ -274,7 +273,6 @@ async def async_setup(hass: HomeAssistant) -> None: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_save_processor_data_at_stop, - run_immediately=True, ) @@ -376,11 +374,9 @@ class PassiveBluetoothProcessorCoordinator( try: update = self._update_method(service_info) - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except self.last_update_success = False - self.logger.exception( - "Unexpected error updating %s data: %s", self.name, err - ) + self.logger.exception("Unexpected error updating %s data", self.name) return if not self.last_update_success: @@ -588,10 +584,10 @@ class PassiveBluetoothDataProcessor(Generic[_T]): """Handle a Bluetooth event.""" try: new_data = self.update_method(update) - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except self.last_update_success = False self.coordinator.logger.exception( - "Unexpected error updating %s data: %s", self.coordinator.name, err + "Unexpected error updating %s data", self.coordinator.name ) return diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 4b168126251..c28bd3cc65e 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{name} {manufacturer} {model}", "step": { "user": { "description": "Choose a device to set up", @@ -18,7 +18,7 @@ } }, "single_adapter": { - "description": "Do you want to set up the Bluetooth adapter {name}?" + "description": "Do you want to set up the Bluetooth adapter {name} {manufacturer} {model}?" } }, "abort": { diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 0faac9a8613..8c7ad13294a 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,7 +2,14 @@ from __future__ import annotations -from bluetooth_adapters import BluetoothAdapters +from bluetooth_adapters import ( + ADAPTER_ADDRESS, + ADAPTER_MANUFACTURER, + ADAPTER_PRODUCT, + AdapterDetails, + BluetoothAdapters, + adapter_unique_name, +) from bluetooth_data_tools import monotonic_time_coarse from homeassistant.core import callback @@ -69,3 +76,12 @@ def async_load_history_from_system( connectable_loaded_history[address] = service_info return all_loaded_history, connectable_loaded_history + + +@callback +def adapter_title(adapter: str, details: AdapterDetails) -> str: + """Return the adapter title.""" + unique_name = adapter_unique_name(adapter, details[ADAPTER_ADDRESS]) + model = details.get(ADAPTER_PRODUCT, "Unknown") + manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown" + return f"{manufacturer} {model} ({unique_name})" diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 85a0cbf8812..d40d85e4cd4 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -116,6 +116,7 @@ class BMWBinarySensorEntityDescription(BinarySensorEntityDescription): value_fn: Callable[[MyBMWVehicle], bool] attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None + is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( @@ -174,12 +175,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.BATTERY_CHARGING, # device class power: On means power detected, Off means no power value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING, + is_available=lambda v: v.has_electric_drivetrain, ), BMWBinarySensorEntityDescription( key="connection_status", translation_key="connection_status", device_class=BinarySensorDeviceClass.PLUG, value_fn=lambda v: v.fuel_and_battery.is_charger_connected, + is_available=lambda v: v.has_electric_drivetrain, ), BMWBinarySensorEntityDescription( key="is_pre_entry_climatization_enabled", @@ -187,6 +190,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled if v.charging_profile else False, + is_available=lambda v: v.has_electric_drivetrain, ), ) @@ -203,7 +207,7 @@ async def async_setup_entry( BMWBinarySensor(coordinator, vehicle, description, hass.config.units) for vehicle in coordinator.account.vehicles for description in SENSOR_TYPES - if description.key in vehicle.available_attributes + if description.is_available(vehicle) ] async_add_entities(entities) diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 49990977f71..5374b52e684 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -28,3 +28,10 @@ SCAN_INTERVALS = { "north_america": 600, "rest_of_world": 300, } + +CLIMATE_ACTIVITY_STATE: list[str] = [ + "cooling", + "heating", + "inactive", + "standby", +] diff --git a/homeassistant/components/bmw_connected_drive/icons.json b/homeassistant/components/bmw_connected_drive/icons.json index a4eb37b369a..fc30b87ed3f 100644 --- a/homeassistant/components/bmw_connected_drive/icons.json +++ b/homeassistant/components/bmw_connected_drive/icons.json @@ -85,6 +85,9 @@ }, "remaining_fuel_percent": { "default": "mdi:gas-station" + }, + "climate_status": { + "default": "mdi:fan" } }, "switch": { diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 9529c135280..bbfadcef9db 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -51,7 +51,7 @@ class BMWLock(BMWBaseEntity, LockEntity): super().__init__(coordinator, vehicle) self._attr_unique_id = f"{vehicle.vin}-lock" - self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes + self.door_lock_state_available = vehicle.is_lsc_enabled async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 854a2f87410..c6b180ca728 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.14.6"] + "requirements": ["bimmer-connected[china]==0.15.2"] } diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 49842305af0..d3366543c55 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import BMWBaseEntity -from .const import DOMAIN, UNIT_MAP +from .const import CLIMATE_ACTIVITY_STATE, DOMAIN, UNIT_MAP from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): key_class: str | None = None unit_type: str | None = None value: Callable = lambda x, y: x + is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled def convert_and_round( @@ -53,57 +54,63 @@ def convert_and_round( return None -SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { +SENSOR_TYPES: list[BMWSensorEntityDescription] = [ # --- Generic --- - "ac_current_limit": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="ac_current_limit", translation_key="ac_current_limit", key_class="charging_profile", unit_type=UnitOfElectricCurrent.AMPERE, entity_registry_enabled_default=False, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_start_time": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_start_time", translation_key="charging_start_time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_end_time": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_end_time", translation_key="charging_end_time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_status": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_status", translation_key="charging_status", key_class="fuel_and_battery", value=lambda x, y: x.value, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_target": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_target", translation_key="charging_target", key_class="fuel_and_battery", unit_type=PERCENTAGE, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "remaining_battery_percent": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_battery_percent", translation_key="remaining_battery_percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), # --- Specific --- - "mileage": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="mileage", translation_key="mileage", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.TOTAL_INCREASING, ), - "remaining_range_total": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_total", translation_key="remaining_range_total", key_class="fuel_and_battery", @@ -111,38 +118,51 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, ), - "remaining_range_electric": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_electric", translation_key="remaining_range_electric", key_class="fuel_and_battery", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "remaining_range_fuel": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_fuel", translation_key="remaining_range_fuel", key_class="fuel_and_battery", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), - "remaining_fuel": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_fuel", translation_key="remaining_fuel", key_class="fuel_and_battery", unit_type=VOLUME, value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), - "remaining_fuel_percent": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_fuel_percent", translation_key="remaining_fuel_percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), -} + BMWSensorEntityDescription( + key="activity", + translation_key="climate_status", + key_class="climate", + device_class=SensorDeviceClass.ENUM, + options=CLIMATE_ACTIVITY_STATE, + value=lambda x, _: x.lower() if x != "UNKNOWN" else None, + is_available=lambda v: v.is_remote_climate_stop_enabled, + ), +] async def async_setup_entry( @@ -153,16 +173,12 @@ async def async_setup_entry( """Set up the MyBMW sensors from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[BMWSensor] = [] - - for vehicle in coordinator.account.vehicles: - entities.extend( - [ - BMWSensor(coordinator, vehicle, description) - for attribute_name in vehicle.available_attributes - if (description := SENSOR_TYPES.get(attribute_name)) - ] - ) + entities = [ + BMWSensor(coordinator, vehicle, description) + for vehicle in coordinator.account.vehicles + for description in SENSOR_TYPES + if description.is_available(vehicle) + ] async_add_entities(entities) diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index 69abd97ddfe..539c281a1a5 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -122,6 +122,15 @@ }, "remaining_fuel_percent": { "name": "Remaining fuel percent" + }, + "climate_status": { + "name": "Climate status", + "state": { + "cooling": "Cooling", + "heating": "Heating", + "inactive": "Inactive", + "standby": "Standby" + } } }, "switch": { diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 216a4b501f2..9ecfedee570 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -68,9 +68,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_stop_event) ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BondData(hub, bpup_subs) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 45170a0404f..a12d3057258 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -113,7 +113,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN): ): updates[CONF_ACCESS_TOKEN] = token return self.async_update_reload_and_abort( - entry, data={**entry.data, **updates}, reason="already_configured" + entry, + data={**entry.data, **updates}, + reason="already_configured", + reload_even_if_entry_is_unchanged=False, ) self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 02137d27b3d..4495e76859d 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -128,7 +128,9 @@ class BondEntity(Entity): _FALLBACK_SCAN_INTERVAL, ) return - self.hass.async_create_task(self._async_update()) + self.hass.async_create_background_task( + self._async_update(), f"{DOMAIN} {self.name} update", eager_start=True + ) async def _async_update(self) -> None: """Fetch via the API.""" diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index efae159adc2..0c99324efbb 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.82"], + "requirements": ["boschshcpy==0.2.91"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/braviatv/diagnostics.py b/homeassistant/components/braviatv/diagnostics.py index 917572ffcca..b74a8a3ebdb 100644 --- a/homeassistant/components/braviatv/diagnostics.py +++ b/homeassistant/components/braviatv/diagnostics.py @@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics( device_info = await coordinator.client.get_system_info() - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "device_info": async_redact_data(device_info, TO_REDACT), } - - return diagnostics_data diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index 7c300a0e013..e408001e458 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -39,23 +39,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await bring.load_lists() except BringRequestException as e: raise ConfigEntryNotReady( - f"Timeout while connecting for email '{email}'" - ) from e - except BringAuthException as e: - _LOGGER.error( - "Authentication failed for '%s', check your email and password", - email, - ) - raise ConfigEntryError( - f"Authentication failed for '{email}', check your email and password" + translation_domain=DOMAIN, + translation_key="setup_request_exception", ) from e except BringParseException as e: - _LOGGER.error( - "Failed to parse request '%s', check your email and password", - email, - ) raise ConfigEntryNotReady( - "Failed to parse response request from server, try again later" + translation_domain=DOMAIN, + translation_key="setup_request_exception", + ) from e + except BringAuthException as e: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_EMAIL: email}, ) from e coordinator = BringDataUpdateCoordinator(hass, bring) diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 0b423f5af36..1fbddeb7bfe 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -9,7 +9,7 @@ from bring_api.bring import Bring from bring_api.exceptions import BringAuthException, BringRequestException 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.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( @@ -38,14 +38,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BringConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bring!.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/bring/const.py b/homeassistant/components/bring/const.py index 64a6ec67f85..911c08a835d 100644 --- a/homeassistant/components/bring/const.py +++ b/homeassistant/components/bring/const.py @@ -1,3 +1,11 @@ """Constants for the Bring! integration.""" +from typing import Final + DOMAIN = "bring" + +ATTR_SENDER: Final = "sender" +ATTR_ITEM_NAME: Final = "item" +ATTR_NOTIFICATION_TYPE: Final = "message" + +SERVICE_PUSH_NOTIFICATION = "send_message" diff --git a/homeassistant/components/bring/icons.json b/homeassistant/components/bring/icons.json index a757b20a4cc..1c6c3bdeca0 100644 --- a/homeassistant/components/bring/icons.json +++ b/homeassistant/components/bring/icons.json @@ -5,5 +5,8 @@ "default": "mdi:cart" } } + }, + "services": { + "send_message": "mdi:cellphone-message" } } diff --git a/homeassistant/components/bring/services.yaml b/homeassistant/components/bring/services.yaml new file mode 100644 index 00000000000..98d5c68de13 --- /dev/null +++ b/homeassistant/components/bring/services.yaml @@ -0,0 +1,23 @@ +send_message: + target: + entity: + domain: todo + integration: bring + fields: + message: + example: urgent_message + required: true + default: "going_shopping" + selector: + select: + translation_key: "notification_type_selector" + options: + - "going_shopping" + - "changed_list" + - "shopping_done" + - "urgent_message" + item: + example: Cilantro + required: false + selector: + text: diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index de3677bf5f1..e6df885cbbc 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -16,5 +16,64 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "exceptions": { + "todo_save_item_failed": { + "message": "Failed to save item {name} to Bring! list" + }, + "todo_update_item_failed": { + "message": "Failed to update item {name} to Bring! list" + }, + "todo_rename_item_failed": { + "message": "Failed to rename item {name} to Bring! list" + }, + "todo_delete_item_failed": { + "message": "Failed to delete {count} item(s) from Bring! list" + }, + "setup_request_exception": { + "message": "Failed to connect to server, try again later" + }, + "setup_parse_exception": { + "message": "Failed to parse server response, try again later" + }, + "setup_authentication_exception": { + "message": "Authentication failed for {email}, check your email and password" + }, + "notify_missing_argument_item": { + "message": "Failed to call service {service}. 'URGENT_MESSAGE' requires a value @ data['item']. Got None" + }, + "notify_request_failed": { + "message": "Failed to send push notification for bring due to a connection error, try again later" + } + }, + "services": { + "send_message": { + "name": "[%key:component::notify::services::notify::name%]", + "description": "Send a mobile push notification to members of a shared Bring! list.", + "fields": { + "entity_id": { + "name": "List", + "description": "Bring! list whose members (except sender) will be notified." + }, + "message": { + "name": "Notification type", + "description": "Type of push notification to send to list members." + }, + "item": { + "name": "Item (Required if message type `Breaking news` selected)", + "description": "Item name to include in a breaking news message e.g. `Breaking news - Please get cilantro!`" + } + } + } + }, + "selector": { + "notification_type_selector": { + "options": { + "going_shopping": "I'm going shopping! - Last chance for adjustments", + "changed_list": "List changed - Check it out", + "shopping_done": "Shopping done - you can relax", + "urgent_message": "Breaking news - Please get `item`!" + } + } } } diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index a1988e667b5..5eabcc01553 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -6,7 +6,8 @@ from typing import TYPE_CHECKING import uuid from bring_api.exceptions import BringRequestException -from bring_api.types import BringItem, BringItemOperation +from bring_api.types import BringItem, BringItemOperation, BringNotificationType +import voluptuous as vol from homeassistant.components.todo import ( TodoItem, @@ -16,11 +17,18 @@ from homeassistant.components.todo import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import ( + ATTR_ITEM_NAME, + ATTR_NOTIFICATION_TYPE, + DOMAIN, + SERVICE_PUSH_NOTIFICATION, +) from .coordinator import BringData, BringDataUpdateCoordinator @@ -46,6 +54,21 @@ async def async_setup_entry( for bring_list in coordinator.data.values() ) + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_PUSH_NOTIFICATION, + make_entity_service_schema( + { + vol.Required(ATTR_NOTIFICATION_TYPE): vol.All( + vol.Upper, cv.enum(BringNotificationType) + ), + vol.Optional(ATTR_ITEM_NAME): cv.string, + } + ), + "async_send_message", + ) + class BringTodoListEntity( CoordinatorEntity[BringDataUpdateCoordinator], TodoListEntity @@ -112,7 +135,11 @@ class BringTodoListEntity( str(uuid.uuid4()), ) except BringRequestException as e: - raise HomeAssistantError("Unable to save todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_save_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e await self.coordinator.async_refresh() @@ -167,7 +194,11 @@ class BringTodoListEntity( else BringItemOperation.COMPLETE, ) except BringRequestException as e: - raise HomeAssistantError("Unable to update todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_update_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e else: try: await self.coordinator.bring.batch_update_list( @@ -191,7 +222,11 @@ class BringTodoListEntity( ) except BringRequestException as e: - raise HomeAssistantError("Unable to replace todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_rename_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e await self.coordinator.async_refresh() @@ -212,6 +247,33 @@ class BringTodoListEntity( BringItemOperation.REMOVE, ) except BringRequestException as e: - raise HomeAssistantError("Unable to delete todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_delete_item_failed", + translation_placeholders={"count": str(len(uids))}, + ) from e await self.coordinator.async_refresh() + + async def async_send_message( + self, + message: BringNotificationType, + item: str | None = None, + ) -> None: + """Send a push notification to members of a shared bring list.""" + + try: + await self.coordinator.bring.notify(self._list_uuid, message, item or None) + except BringRequestException as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="notify_request_failed", + ) from e + except ValueError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="notify_missing_argument_item", + translation_placeholders={ + "service": f"{DOMAIN}.{SERVICE_PUSH_NOTIFICATION}", + }, + ) from e diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 91d4358a077..41c4964c2b3 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -9,6 +9,7 @@ DOMAINS_AND_TYPES = { Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, Platform.SENSOR: { "A1", + "MP1S", "RM4MINI", "RM4PRO", "RMPRO", @@ -20,6 +21,7 @@ DOMAINS_AND_TYPES = { Platform.SWITCH: { "BG1", "MP1", + "MP1S", "RM4MINI", "RM4PRO", "RMMINI", diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 7fd925a2ff4..bf5dfb16584 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -38,5 +38,5 @@ "documentation": "https://www.home-assistant.io/integrations/broadlink", "iot_class": "local_polling", "loggers": ["broadlink"], - "requirements": ["broadlink==0.18.3"] + "requirements": ["broadlink==0.19.0"] } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index f8d903c51eb..55368e5ff59 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -373,7 +373,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): start_time = dt_util.utcnow() while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) - found = await device.async_request(device.api.check_frequency) + found = await device.async_request(device.api.check_frequency)[0] if found: break else: diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index f61e726b1d5..9cf7e3391fa 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -129,7 +129,7 @@ async def async_setup_entry( elif device.api.type == "BG1": switches.extend(BroadlinkBG1Slot(device, slot) for slot in range(1, 3)) - elif device.api.type == "MP1": + elif device.api.type in {"MP1", "MP1S"}: switches.extend(BroadlinkMP1Slot(device, slot) for slot in range(1, 5)) async_add_entities(switches) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 20b241b0d89..f678af0105f 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -21,6 +21,7 @@ def get_update_manager(device): "LB1": BroadlinkLB1UpdateManager, "LB2": BroadlinkLB1UpdateManager, "MP1": BroadlinkMP1UpdateManager, + "MP1S": BroadlinkMP1SUpdateManager, "RM4MINI": BroadlinkRMUpdateManager, "RM4PRO": BroadlinkRMUpdateManager, "RMMINI": BroadlinkRMUpdateManager, @@ -112,6 +113,16 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager): return await self.device.async_request(self.device.api.check_power) +class BroadlinkMP1SUpdateManager(BroadlinkUpdateManager): + """Manages updates for Broadlink MP1 devices.""" + + async def async_fetch_data(self): + """Fetch data from the device.""" + power = await self.device.async_request(self.device.api.check_power) + sensors = await self.device.async_request(self.device.api.get_state) + return {**power, **sensors} + + class BroadlinkRMUpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink remotes.""" diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index a4afb385f8d..ee5eedd84cb 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -20,11 +20,9 @@ async def async_get_config_entry_diagnostics( config_entry.entry_id ] - diagnostics_data = { + return { "info": dict(config_entry.data), "data": asdict(coordinator.data), "model": coordinator.brother.model, "firmware": coordinator.brother.firmware, } - - return diagnostics_data diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index 789a5a48bd9..65886c3081c 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -38,13 +38,13 @@ async def validate_input(user_input: dict[str, Any]) -> dict[str, str] | None: _LOGGER.warning("Brunt Credentials are incorrect") errors = {"base": "invalid_auth"} else: - _LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc) + _LOGGER.exception("Unknown error when trying to login to Brunt") errors = {"base": "unknown"} except ServerDisconnectedError: _LOGGER.warning("Cannot connect to Brunt") errors = {"base": "cannot_connect"} - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error when trying to login to Brunt") errors = {"base": "unknown"} finally: await bapi.async_close() diff --git a/homeassistant/components/bthome/strings.json b/homeassistant/components/bthome/strings.json index 50c5c7bada6..c64028229b3 100644 --- a/homeassistant/components/bthome/strings.json +++ b/homeassistant/components/bthome/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index fb15aa49001..69c762c1bc1 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -819,22 +819,23 @@ class BrSensor(SensorEntity): self._attr_native_value = data.get(FORECAST)[fcday].get( sensor_type[:-3] ) - if self.state is not None: - self._attr_native_value = round(self.state * 3.6, 1) - return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) return False + if self.state is not None: + self._attr_native_value = round(self.state * 3.6, 1) + return True + # update all other sensors try: self._attr_native_value = data.get(FORECAST)[fcday].get( sensor_type[:-3] ) - return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) return False + return True if sensor_type == SYMBOL or sensor_type.startswith(CONDITION): # update weather symbol & status text diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 10589aa461f..cb8ac7745b2 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -24,11 +25,6 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN, SERVICE_PRESS -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 844232c4b22..ad86ab1957d 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -137,7 +137,7 @@ def queued_event_fetcher( # time span, but need to be triggered later when the end happens. results = [] for trigger_time, event in zip( - map(get_trigger_time, active_events), active_events + map(get_trigger_time, active_events), active_events, strict=False ): if trigger_time not in offset_timespan: continue diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index bfeab601352..861b184975b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -9,12 +9,12 @@ from contextlib import suppress from dataclasses import asdict from datetime import datetime, timedelta from enum import IntFlag -from functools import partial +from functools import cached_property, partial import logging import os from random import SystemRandom import time -from typing import TYPE_CHECKING, Any, Final, cast, final +from typing import Any, Final, cast, final from aiohttp import hdrs, web import attr @@ -85,11 +85,6 @@ from .const import ( # noqa: F401 from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" @@ -417,9 +412,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, preload_stream) @callback def update_tokens(t: datetime) -> None: @@ -437,9 +430,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval) component.async_register_entity_service( SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" @@ -520,14 +511,14 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self._create_stream_lock: asyncio.Lock | None = None self._rtsp_to_webrtc = False - @property + @cached_property def entity_picture(self) -> str: """Return a link to the camera feed as entity picture.""" if self._attr_entity_picture is not None: return self._attr_entity_picture return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) - @property + @cached_property def use_stream_for_stills(self) -> bool: """Whether or not to use stream to generate stills.""" return False @@ -754,6 +745,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def async_update_token(self) -> None: """Update the used token.""" self.access_tokens.append(hex(_RND.getrandbits(256))[2:]) + self.__dict__.pop("entity_picture", None) async def async_internal_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 60ce50484d8..f879c308a88 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -140,10 +140,8 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non def _get_canary_api_instance(entry: ConfigEntry) -> Api: """Initialize a new instance of CanaryApi.""" - canary = Api( + return Api( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), ) - - return canary diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index c57b686143d..056ee054d1d 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypedDict -from homeassistant.helpers.dispatcher import SignalType +from homeassistant.util.signal_type import SignalType if TYPE_CHECKING: from .helpers import ChromecastInfo diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 60863523553..8f937ef61ea 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -43,7 +43,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT), ) - return True except ResolveFailed: self._errors[CONF_HOST] = "resolve_failed" except ConnectionTimeout: @@ -52,6 +51,8 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN): self._errors[CONF_HOST] = "connection_refused" except ValidationFailure: return True + else: + return True return False async def async_step_user( diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 002ec8d4efb..2b8fc4a2b3e 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -167,8 +167,7 @@ class ChannelsPlayer(MediaPlayerEntity): @property def source_list(self): """List of favorite channels.""" - sources = [channel["name"] for channel in self.favorite_channels] - return sources + return [channel["name"] for channel in self.favorite_channels] @property def is_volume_muted(self): diff --git a/homeassistant/components/circuit/__init__.py b/homeassistant/components/circuit/__init__.py index f71babad3d5..7e7d0eda76e 100644 --- a/homeassistant/components/circuit/__init__.py +++ b/homeassistant/components/circuit/__init__.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_URL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery +import homeassistant.helpers.issue_registry as ir from homeassistant.helpers.typing import ConfigType DOMAIN = "circuit" @@ -26,6 +27,17 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Unify Circuit component.""" + ir.async_create_issue( + hass, + DOMAIN, + "service_removal", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_removal", + translation_placeholders={"integration": "Unify Circuit", "domain": DOMAIN}, + ) webhooks = config[DOMAIN][CONF_WEBHOOK] for webhook_conf in webhooks: diff --git a/homeassistant/components/circuit/strings.json b/homeassistant/components/circuit/strings.json new file mode 100644 index 00000000000..b9cb852d5b9 --- /dev/null +++ b/homeassistant/components/circuit/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "service_removal": { + "title": "The {integration} integration is being removed", + "description": "The {integration} integration will be removed, as the service is no longer maintained.\n\n\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index c156f43942e..d96ab54a68f 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -72,11 +72,10 @@ class CiscoMEDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next( + return next( (result.clId for result in self.last_results if result.macaddr == device), None, ) - return name def get_extra_attributes(self, device): """Get extra attributes of a device. diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 0cf27c20fa6..4049a656caf 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -201,9 +201,9 @@ async def async_setup_platform( if radius > dist or stations_list.intersection((station_id, station_uid)): if name: - uid = "_".join([network.network_id, name, station_id]) + uid = f"{network.network_id}_{name}_{station_id}" else: - uid = "_".join([network.network_id, station_id]) + uid = f"{network.network_id}_{station_id}" entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, hass=hass) devices.append(CityBikesStation(network, station_id, entity_id)) @@ -228,6 +228,9 @@ class CityBikesNetworks: self.hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA ) self.networks = networks[ATTR_NETWORKS_LIST] + except CityBikesRequestError as err: + raise PlatformNotReady from err + else: result = None minimum_dist = None for network in self.networks: @@ -241,8 +244,6 @@ class CityBikesNetworks: result = network[ATTR_ID] return result - except CityBikesRequestError as err: - raise PlatformNotReady from err finally: self.networks_loading.release() diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 00fd69ce63b..bda00c9b57f 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -5,8 +5,9 @@ from __future__ import annotations import asyncio from datetime import timedelta import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Literal, final +from typing import Any, Literal, final import voluptuous as vol @@ -117,11 +118,6 @@ from .const import ( # noqa: F401 HVACMode, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_HUMIDITY = 30 diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index c790b8596a9..b1bf78063c7 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -33,7 +33,7 @@ class HVACMode(StrEnum): # Device is in Dry/Humidity mode DRY = "dry" - # Only the fan is on, not fan and another mode like cool + # Only the fan is on, not fan and another mode like cool FAN_ONLY = "fan_only" diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index aefab869955..13f1d34b5cd 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -7,11 +7,14 @@ from collections.abc import Awaitable, Callable from datetime import datetime, timedelta from enum import Enum from typing import cast +from urllib.parse import quote_plus, urljoin from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.components import alexa, google_assistant +from homeassistant.components import alexa, google_assistant, http +from homeassistant.components.auth import STRICT_CONNECTION_URL +from homeassistant.components.http.auth import async_sign_path from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import ( CONF_DESCRIPTION, @@ -21,20 +24,33 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import ( + Event, + HassJob, + HomeAssistant, + ServiceCall, + ServiceResponse, + callback, +) +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceValidationError, + Unauthorized, + UnknownUser, +) from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.signal_type import SignalType from . import account_link, http_api from .client import CloudClient @@ -265,18 +281,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) _remote_handle_prefs_updated(cloud) - - async def _service_handler(service: ServiceCall) -> None: - """Handle service for cloud.""" - if service.service == SERVICE_REMOTE_CONNECT: - await prefs.async_update(remote_enabled=True) - elif service.service == SERVICE_REMOTE_DISCONNECT: - await prefs.async_update(remote_enabled=False) - - async_register_admin_service(hass, DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler) - async_register_admin_service( - hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler - ) + _setup_services(hass, prefs) async def async_startup_repairs(_: datetime) -> None: """Create repair issues after startup.""" @@ -343,7 +348,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, {"platform_loaded": tts_platform_loaded}, config, - ) + ), + eager_start=True, ) async_call_later( @@ -393,6 +399,61 @@ 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) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return unload_ok + +@callback +def _setup_services(hass: HomeAssistant, prefs: CloudPreferences) -> None: + """Set up services for cloud component.""" + + async def _service_handler(service: ServiceCall) -> None: + """Handle service for cloud.""" + if service.service == SERVICE_REMOTE_CONNECT: + await prefs.async_update(remote_enabled=True) + elif service.service == SERVICE_REMOTE_DISCONNECT: + await prefs.async_update(remote_enabled=False) + + async_register_admin_service(hass, DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler) + async_register_admin_service( + hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler + ) + + async def create_temporary_strict_connection_url( + call: ServiceCall, + ) -> ServiceResponse: + """Create a strict connection url and return it.""" + # Copied form homeassistant/helpers/service.py#_async_admin_handler + # as the helper supports no responses yet + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + if user is None: + raise UnknownUser(context=call.context) + if not user.is_admin: + raise Unauthorized(context=call.context) + + if prefs.strict_connection is http.const.StrictConnectionMode.DISABLED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="strict_connection_not_enabled", + ) + + try: + url = get_url(hass, require_cloud=True) + except NoURLAvailableError as ex: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_url_available", + ) from ex + + path = async_sign_path( + hass, + STRICT_CONNECTION_URL, + timedelta(hours=1), + use_content_user=True, + ) + url = urljoin(url, path) + + return { + "url": f"https://login.home-assistant.io?u={quote_plus(url)}", + "direct_url": url, + } diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index df2789663c0..784de14e6ad 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -65,7 +65,7 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]: services: list[dict[str, Any]] if DATA_SERVICES in hass.data: services = hass.data[DATA_SERVICES] - return services + return services # noqa: RET504 try: services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 12f2b04d856..5b77a02384d 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -509,18 +509,17 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): try: async with asyncio.timeout(10): await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) - - return True - except TimeoutError: _LOGGER.warning("Timeout trying to sync entities to Alexa") return False - except aiohttp.ClientError as err: _LOGGER.warning("Error trying to sync entities to Alexa: %s", err) return False + return True - async def _handle_entity_registry_updated(self, event: Event) -> None: + async def _handle_entity_registry_updated( + self, event: Event[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle when entity registry updated.""" if not self.enabled or not self._cloud.is_logged_in: return @@ -530,15 +529,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): if not self.should_expose(entity_id): return - action = event.data["action"] - to_update = [] - to_remove = [] + to_update: list[str] = [] + to_remove: list[str] = [] - if action == "create": + if event.data["action"] == "create": to_update.append(entity_id) - elif action == "remove": + elif event.data["action"] == "remove": to_remove.append(entity_id) - elif action == "update" and bool( + elif event.data["action"] == "update" and bool( set(event.data["changes"]) & er.ENTITY_DESCRIBING_ATTRIBUTES ): to_update.append(entity_id) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 01c8de77156..c4d1c1dec60 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -250,6 +250,7 @@ class CloudClient(Interface): "enabled": self._prefs.remote_enabled, "instance_domain": self.cloud.remote.instance_domain, "alias": self.cloud.remote.alias, + "strict_connection": self._prefs.strict_connection, }, "version": HA_VERSION, "instance_id": self.prefs.instance_id, diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 1ee7392eccf..8b68eefc443 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.dispatcher import SignalType +from homeassistant.util.signal_type import SignalType DOMAIN = "cloud" DATA_PLATFORMS_SETUP = "cloud_platforms_setup" @@ -33,6 +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" +PREF_STRICT_CONNECTION = "strict_connection" DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural") DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = True diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 1ba2fab717f..3586823ca11 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -453,7 +453,9 @@ class CloudGoogleConfig(AbstractConfig): self.async_schedule_google_sync_all() @callback - def _handle_entity_registry_updated(self, event: Event) -> None: + def _handle_entity_registry_updated( + self, event: Event[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle when entity registry updated.""" if ( not self.enabled @@ -476,7 +478,9 @@ class CloudGoogleConfig(AbstractConfig): self.async_schedule_google_sync_all() @callback - async def _handle_device_registry_updated(self, event: Event) -> None: + async def _handle_device_registry_updated( + self, event: Event[dr.EventDeviceRegistryUpdatedData] + ) -> None: """Handle when device registry updated.""" if ( not self.enabled diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 1a8fd7dbea9..29185191a20 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -19,7 +19,7 @@ from hass_nabucasa.const import STATE_DISCONNECTED from hass_nabucasa.voice import TTS_VOICES import voluptuous as vol -from homeassistant.components import websocket_api +from homeassistant.components import http, websocket_api from homeassistant.components.alexa import ( entities as alexa_entities, errors as alexa_errors, @@ -46,6 +46,7 @@ from .const import ( PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_REMOTE_ALLOW_REMOTE_ENABLE, + PREF_STRICT_CONNECTION, PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, ) @@ -135,12 +136,12 @@ def _handle_cloud_errors( """Handle exceptions that raise from the wrapped request handler.""" try: result = await handler(view, request, *args, **kwargs) - return result except Exception as err: # pylint: disable=broad-except status, msg = _process_cloud_exception(err, request.path) return view.json_message( msg, status_code=status, message_code=err.__class__.__name__.lower() ) + return result return error_handler @@ -223,7 +224,10 @@ class CloudLoginView(HomeAssistantView): cloud: Cloud[CloudClient] = hass.data[DOMAIN] await cloud.login(data["email"], data["password"]) - new_cloud_pipeline_id = await async_create_cloud_pipeline(hass) + if "assist_pipeline" in hass.config.components: + new_cloud_pipeline_id = await async_create_cloud_pipeline(hass) + else: + new_cloud_pipeline_id = None return self.json({"success": True, "cloud_pipeline": new_cloud_pipeline_id}) @@ -449,6 +453,9 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]: vol.Coerce(tuple), validate_language_voice ), vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool, + vol.Optional(PREF_STRICT_CONNECTION): vol.Coerce( + http.const.StrictConnectionMode + ), } ) @websocket_api.async_response diff --git a/homeassistant/components/cloud/icons.json b/homeassistant/components/cloud/icons.json index 06ee7eb2f19..1a8593388b4 100644 --- a/homeassistant/components/cloud/icons.json +++ b/homeassistant/components/cloud/icons.json @@ -1,5 +1,6 @@ { "services": { + "create_temporary_strict_connection_url": "mdi:login-variant", "remote_connect": "mdi:cloud", "remote_disconnect": "mdi:cloud-off" } diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 49a3fc0bf5c..0d2ee546ad8 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Cloud", "after_dependencies": ["assist_pipeline", "google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], - "dependencies": ["http", "repairs", "webhook"], + "dependencies": ["auth", "http", "repairs", "webhook"], "documentation": "https://www.home-assistant.io/integrations/cloud", "integration_type": "system", "iot_class": "cloud_push", diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index af4e68194d6..b4e692d02c4 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -10,7 +10,7 @@ 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 +from homeassistant.components import http, webhook from homeassistant.components.google_assistant.http import ( async_get_users as async_get_google_assistant_users, ) @@ -44,6 +44,7 @@ from .const import ( PREF_INSTANCE_ID, PREF_REMOTE_ALLOW_REMOTE_ENABLE, PREF_REMOTE_DOMAIN, + PREF_STRICT_CONNECTION, PREF_TTS_DEFAULT_VOICE, PREF_USERNAME, ) @@ -176,6 +177,7 @@ class CloudPreferences: google_settings_version: int | UndefinedType = UNDEFINED, google_connected: bool | UndefinedType = UNDEFINED, remote_allow_remote_enable: bool | UndefinedType = UNDEFINED, + strict_connection: http.const.StrictConnectionMode | UndefinedType = UNDEFINED, ) -> None: """Update user preferences.""" prefs = {**self._prefs} @@ -195,6 +197,7 @@ class CloudPreferences: (PREF_REMOTE_DOMAIN, remote_domain), (PREF_GOOGLE_CONNECTED, google_connected), (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), + (PREF_STRICT_CONNECTION, strict_connection), ): if value is not UNDEFINED: prefs[key] = value @@ -242,6 +245,7 @@ class CloudPreferences: PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, PREF_REMOTE_ALLOW_REMOTE_ENABLE: self.remote_allow_remote_enable, PREF_TTS_DEFAULT_VOICE: self.tts_default_voice, + PREF_STRICT_CONNECTION: self.strict_connection, } @property @@ -358,6 +362,11 @@ class CloudPreferences: """ return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return] + @property + def strict_connection(self) -> http.const.StrictConnectionMode: + """Return the strict connection mode.""" + return http.const.StrictConnectionMode.DISABLED + async def get_cloud_user(self) -> str: """Return ID of Home Assistant Cloud system user.""" user = await self._load_cloud_user() @@ -415,4 +424,5 @@ class CloudPreferences: PREF_REMOTE_DOMAIN: None, PREF_REMOTE_ALLOW_REMOTE_ENABLE: True, PREF_USERNAME: username, + PREF_STRICT_CONNECTION: None, } diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index 1c5a8f1f86d..9042a010589 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -94,7 +94,9 @@ class LegacySubscriptionRepairFlow(RepairsFlow): ) if not self.wait_task: - self.wait_task = self.hass.async_create_task(_async_wait_for_plan_change()) + self.wait_task = self.hass.async_create_task( + _async_wait_for_plan_change(), eager_start=False + ) migration = await async_migrate_paypal_agreement(cloud) return self.async_external_step( step_id="change_plan", diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 16a82a27c1a..1fec87235da 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -5,6 +5,14 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "exceptions": { + "strict_connection_not_enabled": { + "message": "Strict connection is not enabled for cloud requests" + }, + "no_url_available": { + "message": "No cloud URL available.\nPlease mark sure you have a working Remote UI." + } + }, "system_health": { "info": { "can_reach_cert_server": "Reach Certificate Server", @@ -73,6 +81,10 @@ } }, "services": { + "create_temporary_strict_connection_url": { + "name": "Create a temporary strict connection URL", + "description": "Create a temporary strict connection URL, which can be used to login on another device." + }, "remote_connect": { "name": "Remote connect", "description": "Makes the instance UI accessible from outside of the local network by using Home Assistant Cloud." diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index d718cc5201e..c68e9f245ee 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient @@ -86,9 +87,18 @@ class CloudProviderEntity(SpeechToTextEntity): async def async_added_to_hass(self) -> None: """Run when entity is about to be added to hass.""" - await async_migrate_cloud_pipeline_engine( - self.hass, platform=Platform.STT, engine_id=self.entity_id - ) + + async def pipeline_setup(hass: HomeAssistant, _comp: str) -> None: + """When assist_pipeline is set up.""" + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + hass, + async_migrate_cloud_pipeline_engine( + self.hass, platform=Platform.STT, engine_id=self.entity_id + ), + ) + + async_when_setup(self.hass, "assist_pipeline", pipeline_setup) async def async_process_audio_stream( self, metadata: SpeechMetadata, stream: AsyncIterable[bytes] diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 42e4b94a189..53cec74d133 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -27,6 +27,7 @@ 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.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient @@ -156,9 +157,19 @@ class CloudTTSEntity(TextToSpeechEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - await async_migrate_cloud_pipeline_engine( - self.hass, platform=Platform.TTS, engine_id=self.entity_id - ) + + async def pipeline_setup(hass: HomeAssistant, _comp: str) -> None: + """When assist_pipeline is set up.""" + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + hass, + async_migrate_cloud_pipeline_engine( + self.hass, platform=Platform.TTS, engine_id=self.entity_id + ), + ) + + async_when_setup(self.hass, "assist_pipeline", pipeline_setup) + self.async_on_remove( self.cloud.client.prefs.async_listen_updates(self._sync_prefs) ) diff --git a/homeassistant/components/cloud/util.py b/homeassistant/components/cloud/util.py new file mode 100644 index 00000000000..3e055851fff --- /dev/null +++ b/homeassistant/components/cloud/util.py @@ -0,0 +1,15 @@ +"""Cloud util functions.""" + +from hass_nabucasa import Cloud + +from homeassistant.components import http +from homeassistant.core import HomeAssistant + +from .client import CloudClient +from .const import DOMAIN + + +def get_strict_connection_mode(hass: HomeAssistant) -> http.const.StrictConnectionMode: + """Get the strict connection mode.""" + cloud: Cloud[CloudClient] = hass.data[DOMAIN] + return cloud.client.prefs.strict_connection diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index dafa50bafcb..71ebcec65ee 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -50,8 +50,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( def get_user_from_client(api_key, api_token): """Get the user name from Coinbase API credentials.""" client = Client(api_key, api_token) - user = client.get_current_user() - return user + return client.get_current_user() async def validate_api(hass: HomeAssistant, data): diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index 56270ce0f75..81cd55564b9 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, LIGHT_TURN_ON_SCHEMA, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import aiohttp_client @@ -25,10 +25,7 @@ from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - {vol.Optional(DOMAIN): {}}, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) # Extend the existing light.turn_on service schema SERVICE_SCHEMA = vol.All( @@ -66,19 +63,6 @@ def _get_color(file_handler) -> tuple: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Color extractor component.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={} - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Load a config entry.""" - async def async_handle_service(service_call: ServiceCall) -> None: """Decide which color_extractor method to call based on service.""" service_data = dict(service_call.data) @@ -170,3 +154,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return _get_color(_file) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + return True diff --git a/homeassistant/components/color_extractor/config_flow.py b/homeassistant/components/color_extractor/config_flow.py index de1f9cb35be..aab56eb9537 100644 --- a/homeassistant/components/color_extractor/config_flow.py +++ b/homeassistant/components/color_extractor/config_flow.py @@ -5,9 +5,6 @@ from __future__ import annotations from typing import Any from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DEFAULT_NAME, DOMAIN @@ -21,45 +18,6 @@ class ColorExtractorConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> 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]) -> ConfigFlowResult: - """Handle import from configuration.yaml.""" - result = await self.async_step_user(user_input) - if result["type"] == FlowResultType.CREATE_ENTRY: - 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": "Color extractor", - }, - ) - else: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml", - 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": "Color extractor", - }, - ) - return result diff --git a/homeassistant/components/color_extractor/manifest.json b/homeassistant/components/color_extractor/manifest.json index c87ac2540a6..a86adaac495 100644 --- a/homeassistant/components/color_extractor/manifest.json +++ b/homeassistant/components/color_extractor/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@GenericStudent"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/color_extractor", - "requirements": ["colorthief==0.2.1"] + "requirements": ["colorthief==0.2.1"], + "single_config_entry": true } diff --git a/homeassistant/components/color_extractor/strings.json b/homeassistant/components/color_extractor/strings.json index aa5fd5f4ef7..e501879e881 100644 --- a/homeassistant/components/color_extractor/strings.json +++ b/homeassistant/components/color_extractor/strings.json @@ -4,15 +4,6 @@ "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - } - }, - "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." } }, "services": { diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index d93ec349bba..b9264d16f69 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -4,7 +4,9 @@ "codeowners": ["@chemelli74"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/comelit", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aiocomelit"], + "quality_scale": "silver", "requirements": ["aiocomelit==0.9.0"] } diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 27b69e59ca4..0f217eb0ee1 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -102,6 +102,7 @@ COVER_SCHEMA = vol.Schema( vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index a31f8205a28..2ff17e86efd 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -6,33 +6,24 @@ import asyncio from datetime import datetime, timedelta from typing import cast -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( CONF_COMMAND, - CONF_DEVICE_CLASS, - CONF_ICON, CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - ManualTriggerEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .sensor import CommandSensorData DEFAULT_NAME = "Binary Command Sensor" @@ -53,31 +44,24 @@ async def async_setup_platform( discovery_info = cast(DiscoveryInfoType, discovery_info) binary_sensor_config = discovery_info - name: str = binary_sensor_config.get(CONF_NAME, DEFAULT_NAME) command: str = binary_sensor_config[CONF_COMMAND] payload_off: str = binary_sensor_config[CONF_PAYLOAD_OFF] payload_on: str = binary_sensor_config[CONF_PAYLOAD_ON] - device_class: BinarySensorDeviceClass | None = binary_sensor_config.get( - CONF_DEVICE_CLASS - ) - icon: Template | None = binary_sensor_config.get(CONF_ICON) - value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT] - unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID) scan_interval: timedelta = binary_sensor_config.get( CONF_SCAN_INTERVAL, SCAN_INTERVAL ) - availability: Template | None = binary_sensor_config.get(CONF_AVAILABILITY) - if value_template is not None: + + if value_template := binary_sensor_config.get(CONF_VALUE_TEMPLATE): value_template.hass = hass + data = CommandSensorData(hass, command, command_timeout) trigger_entity_config = { - CONF_UNIQUE_ID: unique_id, - CONF_NAME: Template(name, hass), - CONF_DEVICE_CLASS: device_class, - CONF_ICON: icon, - CONF_AVAILABILITY: availability, + CONF_NAME: Template(binary_sensor_config.get(CONF_NAME, DEFAULT_NAME), hass), + **{ + k: v for k, v in binary_sensor_config.items() if k in TRIGGER_ENTITY_OPTIONS + }, } async_add_entities( diff --git a/homeassistant/components/command_line/const.py b/homeassistant/components/command_line/const.py index ff51cb7e331..0448180dc33 100644 --- a/homeassistant/components/command_line/const.py +++ b/homeassistant/components/command_line/const.py @@ -2,7 +2,18 @@ import logging -from homeassistant.const import Platform +from homeassistant.components.sensor import CONF_STATE_CLASS +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + Platform, +) +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, +) LOGGER = logging.getLogger(__package__) @@ -15,3 +26,13 @@ PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] + +TRIGGER_ENTITY_OPTIONS = { + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_STATE_CLASS, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +} diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index c27cd97b39a..6400be7d92f 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -14,21 +14,17 @@ from homeassistant.const import ( CONF_COMMAND_STOP, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - ManualTriggerEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .utils import async_call_shell_with_timeout, async_check_output_or_log SCAN_INTERVAL = timedelta(seconds=15) @@ -44,29 +40,29 @@ async def async_setup_platform( covers = [] discovery_info = cast(DiscoveryInfoType, discovery_info) - entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info} + entities: dict[str, dict[str, Any]] = { + slugify(discovery_info[CONF_NAME]): discovery_info + } - for device_name, device_config in entities.items(): - value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + for device_name, cover_config in entities.items(): + if value_template := cover_config.get(CONF_VALUE_TEMPLATE): value_template.hass = hass trigger_entity_config = { - CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), - CONF_NAME: Template(device_config.get(CONF_NAME, device_name), hass), - CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), + CONF_NAME: Template(cover_config.get(CONF_NAME, device_name), hass), + **{k: v for k, v in cover_config.items() if k in TRIGGER_ENTITY_OPTIONS}, } covers.append( CommandCover( trigger_entity_config, - device_config[CONF_COMMAND_OPEN], - device_config[CONF_COMMAND_CLOSE], - device_config[CONF_COMMAND_STOP], - device_config.get(CONF_COMMAND_STATE), + cover_config[CONF_COMMAND_OPEN], + cover_config[CONF_COMMAND_CLOSE], + cover_config[CONF_COMMAND_STOP], + cover_config.get(CONF_COMMAND_STATE), value_template, - device_config[CONF_COMMAND_TIMEOUT], - device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + cover_config[CONF_COMMAND_TIMEOUT], + cover_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) ) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 4cfd9af0811..b0c2ca7cb66 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -8,16 +8,12 @@ from datetime import datetime, timedelta import json from typing import Any, cast -from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_COMMAND, - CONF_DEVICE_CLASS, - CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, - CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant @@ -25,31 +21,17 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - CONF_PICTURE, - ManualTriggerSensorEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .utils import async_check_output_or_log CONF_JSON_ATTRIBUTES = "json_attributes" DEFAULT_NAME = "Command Sensor" -TRIGGER_ENTITY_OPTIONS = ( - CONF_AVAILABILITY, - CONF_DEVICE_CLASS, - CONF_ICON, - CONF_PICTURE, - CONF_UNIQUE_ID, - CONF_STATE_CLASS, - CONF_UNIT_OF_MEASUREMENT, -) - SCAN_INTERVAL = timedelta(seconds=60) @@ -64,21 +46,19 @@ async def async_setup_platform( discovery_info = cast(DiscoveryInfoType, discovery_info) sensor_config = discovery_info - name: str = sensor_config[CONF_NAME] command: str = sensor_config[CONF_COMMAND] - value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] - if value_template is not None: - value_template.hass = hass json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) data = CommandSensorData(hass, command, command_timeout) - trigger_entity_config = {CONF_NAME: Template(name, hass)} - for key in TRIGGER_ENTITY_OPTIONS: - if key not in sensor_config: - continue - trigger_entity_config[key] = sensor_config[key] + if value_template := sensor_config.get(CONF_VALUE_TEMPLATE): + value_template.hass = hass + + trigger_entity_config = { + CONF_NAME: Template(sensor_config[CONF_NAME], hass), + **{k: v for k, v in sensor_config.items() if k in TRIGGER_ENTITY_OPTIONS}, + } async_add_entities( [ diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index f84c55d0320..fee94424fa1 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -11,24 +11,19 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE, - CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - ManualTriggerEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .utils import async_call_shell_with_timeout, async_check_output_or_log SCAN_INTERVAL = timedelta(seconds=30) @@ -42,34 +37,31 @@ async def async_setup_platform( ) -> None: """Find and return switches controlled by shell commands.""" - discovery_info = cast(DiscoveryInfoType, discovery_info) - entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info} - switches = [] + discovery_info = cast(DiscoveryInfoType, discovery_info) + entities: dict[str, dict[str, Any]] = { + slugify(discovery_info[CONF_NAME]): discovery_info + } - for object_id, device_config in entities.items(): - trigger_entity_config = { - CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), - CONF_NAME: Template(device_config.get(CONF_NAME, object_id), hass), - CONF_ICON: device_config.get(CONF_ICON), - CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), - } - - value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) - - if value_template is not None: + for object_id, switch_config in entities.items(): + if value_template := switch_config.get(CONF_VALUE_TEMPLATE): value_template.hass = hass + trigger_entity_config = { + CONF_NAME: Template(switch_config.get(CONF_NAME, object_id), hass), + **{k: v for k, v in switch_config.items() if k in TRIGGER_ENTITY_OPTIONS}, + } + switches.append( CommandSwitch( trigger_entity_config, object_id, - device_config[CONF_COMMAND_ON], - device_config[CONF_COMMAND_OFF], - device_config.get(CONF_COMMAND_STATE), + switch_config[CONF_COMMAND_ON], + switch_config[CONF_COMMAND_OFF], + switch_config.get(CONF_COMMAND_STATE), value_template, - device_config[CONF_COMMAND_TIMEOUT], - device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + switch_config[CONF_COMMAND_TIMEOUT], + switch_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) ) diff --git a/homeassistant/components/command_line/utils.py b/homeassistant/components/command_line/utils.py index 067efc08e97..c1926546950 100644 --- a/homeassistant/components/command_line/utils.py +++ b/homeassistant/components/command_line/utils.py @@ -25,20 +25,21 @@ async def async_call_shell_with_timeout( ) 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.returncode, - command, - ) - return return_code or 0 except TimeoutError: _LOGGER.error("Timeout for command: %s", command) return -1 + 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.returncode, + command, + ) + return return_code or 0 + async def async_check_output_or_log(command: str, timeout: int) -> str | None: """Run a shell command with a timeout and return the output.""" diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index dc1f903e8f6..fae416e7fc2 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -90,7 +90,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sorted_coefficients = sorted(initial_coefficients, key=itemgetter(0)) # get x values and y values from the x,y point pairs - x_values, y_values = zip(*initial_coefficients) + x_values, y_values = zip(*initial_coefficients, strict=False) # try to get valid coefficients for a polynomial coefficients = None diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 11d838e2467..95695932540 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -18,12 +18,15 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, - async_track_state_change_event, + HomeAssistant, + State, + callback, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index d71a00ce3bd..1f6dc2c2122 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -7,7 +7,7 @@ from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import ATTR_COMPONENT +from homeassistant.setup import EventComponentLoaded from . import ( area_registry, @@ -56,6 +56,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if panel.async_setup(hass): name = panel.__name__.split(".")[-1] key = f"{DOMAIN}.{name}" - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) + hass.bus.async_fire( + EVENT_COMPONENT_LOADED, EventComponentLoaded(component=key) + ) return True diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index a5a010c00a6..ccc36dc4430 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -26,7 +26,9 @@ def async_setup(hass: HomeAssistant) -> bool: async def hook(action: str, config_key: str) -> None: """post_write_hook for Config View that reloads automations.""" if action != ACTION_DELETE: - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {CONF_ID: config_key} + ) return ent_reg = er.async_get(hass) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 0579df90dc9..b2cf9a136cc 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -250,7 +250,7 @@ class Configurator: # field validation goes here? if callback and ( - job := self.hass.async_add_hass_job( + job := self.hass.async_run_hass_job( HassJob(callback), call.data.get(ATTR_FIELDS, {}) ) ): diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index b8d195fcb05..86a13de1ac8 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + API_RETRY_TIMES, CONF_ACCOUNT, CONF_CONFIG_LISTENER, CONF_CONTROLLER_UNIQUE_ID, @@ -47,6 +48,17 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER] +async def call_c4_api_retry(func, *func_args): + """Call C4 API function and retry on failure.""" + for i in range(API_RETRY_TIMES): + try: + return await func(*func_args) + except client_exceptions.ClientError as exception: + _LOGGER.error("Error connecting to Control4 account API: %s", exception) + if i == API_RETRY_TIMES - 1: + raise ConfigEntryNotReady(exception) from exception + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Control4 from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -74,18 +86,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: controller_unique_id = config[CONF_CONTROLLER_UNIQUE_ID] entry_data[CONF_CONTROLLER_UNIQUE_ID] = controller_unique_id - director_token_dict = await account.getDirectorBearerToken(controller_unique_id) - director_session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) + director_token_dict = await call_c4_api_retry( + account.getDirectorBearerToken, controller_unique_id + ) + director_session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) director = C4Director( config[CONF_HOST], director_token_dict[CONF_TOKEN], director_session ) entry_data[CONF_DIRECTOR] = director - # Add Control4 controller to device registry - controller_href = (await account.getAccountControllers())["href"] - entry_data[CONF_DIRECTOR_SW_VERSION] = await account.getControllerOSVersion( - controller_href + controller_href = (await call_c4_api_retry(account.getAccountControllers))["href"] + entry_data[CONF_DIRECTOR_SW_VERSION] = await call_c4_api_retry( + account.getControllerOSVersion, controller_href ) _, model, mac_address = controller_unique_id.split("_", 3) diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 2d7c6ade255..4ecc1ebe3f5 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -68,9 +68,9 @@ class Control4Validator: self.director_bearer_token = ( await account.getDirectorBearerToken(self.controller_unique_id) )["token"] - return True except (Unauthorized, NotFound): return False + return True async def connect_to_director(self) -> bool: """Test if we can connect to the local Control4 Director.""" @@ -82,10 +82,10 @@ class Control4Validator: self.host, self.director_bearer_token, director_session ) await director.getAllItemInfo() - return True except (Unauthorized, ClientError, TimeoutError): _LOGGER.error("Failed to connect to the Control4 controller") return False + return True class Control4ConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/control4/const.py b/homeassistant/components/control4/const.py index f8d939e1ac5..57074c00108 100644 --- a/homeassistant/components/control4/const.py +++ b/homeassistant/components/control4/const.py @@ -5,6 +5,8 @@ DOMAIN = "control4" DEFAULT_SCAN_INTERVAL = 5 MIN_SCAN_INTERVAL = 1 +API_RETRY_TIMES = 5 + CONF_ACCOUNT = "account" CONF_DIRECTOR = "director" CONF_DIRECTOR_SW_VERSION = "director_sw_version" diff --git a/homeassistant/components/control4/director_utils.py b/homeassistant/components/control4/director_utils.py index 2ce03c2e635..10e9486ee89 100644 --- a/homeassistant/components/control4/director_utils.py +++ b/homeassistant/components/control4/director_utils.py @@ -1,7 +1,6 @@ """Provides data updates from the Control4 controller for platforms.""" from collections import defaultdict -from collections.abc import Set import logging from typing import Any @@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) async def _update_variables_for_config_entry( - hass: HomeAssistant, entry: ConfigEntry, variable_names: Set[str] + hass: HomeAssistant, entry: ConfigEntry, variable_names: set[str] ) -> dict[int, dict[str, Any]]: """Retrieve data from the Control4 director.""" director: C4Director = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR] @@ -32,7 +31,7 @@ async def _update_variables_for_config_entry( async def update_variables_for_config_entry( - hass: HomeAssistant, entry: ConfigEntry, variable_names: Set[str] + hass: HomeAssistant, entry: ConfigEntry, variable_names: set[str] ) -> dict[int, dict[str, Any]]: """Try to Retrieve data from the Control4 director for update_coordinator.""" try: diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index dd8fb967824..333fb24498b 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -2,52 +2,52 @@ from __future__ import annotations -import asyncio -from collections.abc import Iterable -from dataclasses import dataclass import logging import re -from typing import Any, Literal +from typing import Literal -from aiohttp import web -from hassil.recognize import ( - MISSING_ENTITY, - RecognizeResult, - UnmatchedRangeEntity, - UnmatchedTextEntity, -) import voluptuous as vol -from homeassistant import core -from homeassistant.components import http, websocket_api -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.config_entries import ConfigEntry from homeassistant.const import MATCH_ALL -from homeassistant.core import HomeAssistant +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, intent, singleton +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from homeassistant.util import language as language_util -from .agent import AbstractConversationAgent, ConversationInput, ConversationResult -from .const import HOME_ASSISTANT_AGENT -from .default_agent import ( - METADATA_CUSTOM_FILE, - METADATA_CUSTOM_SENTENCE, - DefaultAgent, - SentenceTriggerResult, - async_setup as async_setup_default_agent, +from .agent_manager import ( + AgentInfo, + agent_id_validator, + async_converse, + async_get_agent, + get_agent_manager, ) +from .const import HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT +from .default_agent import async_get_default_agent, async_setup_default_agent +from .entity import ConversationEntity +from .http import async_setup as async_setup_conversation_http +from .models import AbstractConversationAgent, ConversationInput, ConversationResult __all__ = [ "DOMAIN", "HOME_ASSISTANT_AGENT", + "OLD_HOME_ASSISTANT_AGENT", "async_converse", "async_get_agent_info", "async_set_agent", - "async_unset_agent", "async_setup", + "async_unset_agent", + "ConversationEntity", + "ConversationInput", + "ConversationResult", ] _LOGGER = logging.getLogger(__name__) @@ -60,21 +60,11 @@ ATTR_CONVERSATION_ID = "conversation_id" DOMAIN = "conversation" REGEX_TYPE = type(re.compile("")) -DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" SERVICE_RELOAD = "reload" -def agent_id_validator(value: Any) -> str: - """Validate agent ID.""" - hass = core.async_get_hass() - manager = _get_agent_manager(hass) - if not manager.async_is_valid_agent_id(cv.string(value)): - raise vol.Invalid("invalid agent ID") - return value - - SERVICE_PROCESS_SCHEMA = vol.Schema( { vol.Required(ATTR_TEXT): cv.string, @@ -106,34 +96,25 @@ CONFIG_SCHEMA = vol.Schema( ) -@singleton.singleton("conversation_agent") -@core.callback -def _get_agent_manager(hass: HomeAssistant) -> AgentManager: - """Get the active agent.""" - manager = AgentManager(hass) - manager.async_setup() - return manager - - -@core.callback +@callback @bind_hass def async_set_agent( - hass: core.HomeAssistant, + hass: HomeAssistant, config_entry: ConfigEntry, agent: AbstractConversationAgent, ) -> None: """Set the agent to handle the conversations.""" - _get_agent_manager(hass).async_set_agent(config_entry.entry_id, agent) + get_agent_manager(hass).async_set_agent(config_entry.entry_id, agent) -@core.callback +@callback @bind_hass def async_unset_agent( - hass: core.HomeAssistant, + hass: HomeAssistant, config_entry: ConfigEntry, ) -> None: """Set the agent to handle the conversations.""" - _get_agent_manager(hass).async_unset_agent(config_entry.entry_id) + get_agent_manager(hass).async_unset_agent(config_entry.entry_id) async def async_get_conversation_languages( @@ -145,17 +126,27 @@ async def async_get_conversation_languages( If no agent is specified, return a set with the union of languages supported by all conversation agents. """ - agent_manager = _get_agent_manager(hass) + agent_manager = get_agent_manager(hass) + entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] languages: set[str] = set() + agents: list[ConversationEntity | AbstractConversationAgent] + + if agent_id: + agent = async_get_agent(hass, agent_id) + + if agent is None: + raise ValueError(f"Agent {agent_id} not found") + + agents = [agent] - agent_ids: Iterable[str] - if agent_id is None: - agent_ids = iter(info.id for info in agent_manager.async_get_agent_info()) else: - agent_ids = (agent_id,) + agents = list(entity_component.entities) + for info in agent_manager.async_get_agent_info(): + agent = agent_manager.async_get_agent(info.id) + assert agent is not None + agents.append(agent) - for _agent_id in agent_ids: - agent = await agent_manager.async_get_agent(_agent_id) + for agent in agents: if agent.supported_languages == MATCH_ALL: return MATCH_ALL for language_tag in agent.supported_languages: @@ -164,14 +155,50 @@ async def async_get_conversation_languages( return languages +@callback +def async_get_agent_info( + hass: HomeAssistant, + agent_id: str | None = None, +) -> AgentInfo | None: + """Get information on the agent or None if not found.""" + agent = async_get_agent(hass, agent_id) + + if agent is None: + return None + + if isinstance(agent, ConversationEntity): + name = agent.name + if not isinstance(name, str): + name = agent.entity_id + return AgentInfo(id=agent.entity_id, name=name) + + manager = get_agent_manager(hass) + + for agent_info in manager.async_get_agent_info(): + if agent_info.id == agent_id: + return agent_info + + return None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" - agent_manager = _get_agent_manager(hass) + entity_component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) - if config_intents := config.get(DOMAIN, {}).get("intents"): - hass.data[DATA_CONFIG] = config_intents + await async_setup_default_agent( + hass, entity_component, config.get(DOMAIN, {}).get("intents", {}) + ) - async def handle_process(service: core.ServiceCall) -> core.ServiceResponse: + # Temporary migration. We can remove this in 2024.10 + from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel + async_migrate_engine, + ) + + async_migrate_engine( + hass, "conversation", OLD_HOME_ASSISTANT_AGENT, HOME_ASSISTANT_AGENT + ) + + async def handle_process(service: ServiceCall) -> ServiceResponse: """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) @@ -192,9 +219,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return None - async def handle_reload(service: core.ServiceCall) -> None: + async def handle_reload(service: ServiceCall) -> None: """Reload intents.""" - agent = await agent_manager.async_get_agent() + agent = async_get_default_agent(hass) await agent.async_reload(language=service.data.get(ATTR_LANGUAGE)) hass.services.async_register( @@ -202,440 +229,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_PROCESS, handle_process, schema=SERVICE_PROCESS_SCHEMA, - supports_response=core.SupportsResponse.OPTIONAL, + supports_response=SupportsResponse.OPTIONAL, ) hass.services.async_register( DOMAIN, SERVICE_RELOAD, handle_reload, schema=SERVICE_RELOAD_SCHEMA ) - hass.http.register_view(ConversationProcessView()) - websocket_api.async_register_command(hass, websocket_process) - websocket_api.async_register_command(hass, websocket_prepare) - websocket_api.async_register_command(hass, websocket_list_agents) - websocket_api.async_register_command(hass, websocket_hass_agent_debug) + async_setup_conversation_http(hass) return True -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/process", - vol.Required("text"): str, - vol.Optional("conversation_id"): vol.Any(str, None), - vol.Optional("language"): str, - vol.Optional("agent_id"): agent_id_validator, - } -) -@websocket_api.async_response -async def websocket_process( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Process text.""" - result = await async_converse( - hass=hass, - text=msg["text"], - conversation_id=msg.get("conversation_id"), - context=connection.context(msg), - language=msg.get("language"), - agent_id=msg.get("agent_id"), - ) - connection.send_result(msg["id"], result.as_dict()) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -@websocket_api.websocket_command( - { - "type": "conversation/prepare", - vol.Optional("language"): str, - vol.Optional("agent_id"): agent_id_validator, - } -) -@websocket_api.async_response -async def websocket_prepare( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Reload intents.""" - manager = _get_agent_manager(hass) - agent = await manager.async_get_agent(msg.get("agent_id")) - await agent.async_prepare(msg.get("language")) - connection.send_result(msg["id"]) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/agent/list", - vol.Optional("language"): str, - vol.Optional("country"): str, - } -) -@websocket_api.async_response -async def websocket_list_agents( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -) -> None: - """List conversation agents and, optionally, if they support a given language.""" - manager = _get_agent_manager(hass) - - country = msg.get("country") - language = msg.get("language") - agents = [] - - for agent_info in manager.async_get_agent_info(): - agent = await manager.async_get_agent(agent_info.id) - - supported_languages = agent.supported_languages - if language and supported_languages != MATCH_ALL: - supported_languages = language_util.matches( - language, supported_languages, country - ) - - agent_dict: dict[str, Any] = { - "id": agent_info.id, - "name": agent_info.name, - "supported_languages": supported_languages, - } - agents.append(agent_dict) - - connection.send_message(websocket_api.result_message(msg["id"], {"agents": agents})) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/agent/homeassistant/debug", - vol.Required("sentences"): [str], - vol.Optional("language"): str, - vol.Optional("device_id"): vol.Any(str, None), - } -) -@websocket_api.async_response -async def websocket_hass_agent_debug( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -) -> None: - """Return intents that would be matched by the default agent for a list of sentences.""" - agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) - assert isinstance(agent, DefaultAgent) - results = [ - await agent.async_recognize( - ConversationInput( - text=sentence, - context=connection.context(msg), - conversation_id=None, - device_id=msg.get("device_id"), - language=msg.get("language", hass.config.language), - ) - ) - for sentence in msg["sentences"] - ] - - # Return results for each sentence in the same order as the input. - result_dicts: list[dict[str, Any] | None] = [] - for result in results: - result_dict: dict[str, Any] | None = None - if isinstance(result, SentenceTriggerResult): - result_dict = { - # Matched a user-defined sentence trigger. - # We can't provide the response here without executing the - # trigger. - "match": True, - "source": "trigger", - "sentence_template": result.sentence_template or "", - } - elif isinstance(result, RecognizeResult): - successful_match = not result.unmatched_entities - result_dict = { - # Name of the matching intent (or the closest) - "intent": { - "name": result.intent.name, - }, - # Slot values that would be received by the intent - "slots": { # direct access to values - entity_key: entity.text or entity.value - for entity_key, entity in result.entities.items() - }, - # Extra slot details, such as the originally matched text - "details": { - entity_key: { - "name": entity.name, - "value": entity.value, - "text": entity.text, - } - for entity_key, entity in result.entities.items() - }, - # Entities/areas/etc. that would be targeted - "targets": {}, - # True if match was successful - "match": successful_match, - # Text of the sentence template that matched (or was closest) - "sentence_template": "", - # When match is incomplete, this will contain the best slot guesses - "unmatched_slots": _get_unmatched_slots(result), - } - - if successful_match: - result_dict["targets"] = { - state.entity_id: {"matched": is_matched} - for state, is_matched in _get_debug_targets(hass, result) - } - - if result.intent_sentence is not None: - result_dict["sentence_template"] = result.intent_sentence.text - - # Inspect metadata to determine if this matched a custom sentence - if result.intent_metadata and result.intent_metadata.get( - METADATA_CUSTOM_SENTENCE - ): - result_dict["source"] = "custom" - result_dict["file"] = result.intent_metadata.get(METADATA_CUSTOM_FILE) - else: - result_dict["source"] = "builtin" - - result_dicts.append(result_dict) - - connection.send_result(msg["id"], {"results": result_dicts}) - - -def _get_debug_targets( - hass: HomeAssistant, - result: RecognizeResult, -) -> Iterable[tuple[core.State, bool]]: - """Yield state/is_matched pairs for a hassil recognition.""" - entities = result.entities - - name: str | None = None - area_name: str | None = None - domains: set[str] | None = None - device_classes: set[str] | None = None - state_names: set[str] | None = None - - if "name" in entities: - name = str(entities["name"].value) - - if "area" in entities: - area_name = str(entities["area"].value) - - if "domain" in entities: - domains = set(cv.ensure_list(entities["domain"].value)) - - if "device_class" in entities: - device_classes = set(cv.ensure_list(entities["device_class"].value)) - - if "state" in entities: - # HassGetState only - state_names = set(cv.ensure_list(entities["state"].value)) - - if ( - (name is None) - and (area_name is None) - and (not domains) - and (not device_classes) - and (not state_names) - ): - # Avoid "matching" all entities when there is no filter - return - - states = intent.async_match_states( - hass, - name=name, - area_name=area_name, - domains=domains, - device_classes=device_classes, - ) - - for state in states: - # For queries, a target is "matched" based on its state - is_matched = (state_names is None) or (state.state in state_names) - yield state, is_matched - - -def _get_unmatched_slots( - result: RecognizeResult, -) -> dict[str, str | int]: - """Return a dict of unmatched text/range slot entities.""" - unmatched_slots: dict[str, str | int] = {} - for entity in result.unmatched_entities_list: - if isinstance(entity, UnmatchedTextEntity): - if entity.text == MISSING_ENTITY: - # Don't report since these are just missing context - # slots. - continue - - unmatched_slots[entity.name] = entity.text - elif isinstance(entity, UnmatchedRangeEntity): - unmatched_slots[entity.name] = entity.value - - return unmatched_slots - - -class ConversationProcessView(http.HomeAssistantView): - """View to process text.""" - - url = "/api/conversation/process" - name = "api:conversation:process" - - @RequestDataValidator( - vol.Schema( - { - vol.Required("text"): str, - vol.Optional("conversation_id"): str, - vol.Optional("language"): str, - vol.Optional("agent_id"): agent_id_validator, - } - ) - ) - async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: - """Send a request for processing.""" - hass = request.app[http.KEY_HASS] - - result = await async_converse( - hass, - text=data["text"], - conversation_id=data.get("conversation_id"), - context=self.context(request), - language=data.get("language"), - agent_id=data.get("agent_id"), - ) - - return self.json(result.as_dict()) - - -@dataclass(frozen=True) -class AgentInfo: - """Container for conversation agent info.""" - - id: str - name: str - - -@core.callback -def async_get_agent_info( - hass: core.HomeAssistant, - agent_id: str | None = None, -) -> AgentInfo | None: - """Get information on the agent or None if not found.""" - manager = _get_agent_manager(hass) - - if agent_id is None: - agent_id = manager.default_agent - - for agent_info in manager.async_get_agent_info(): - if agent_info.id == agent_id: - return agent_info - - return None - - -async def async_converse( - hass: core.HomeAssistant, - text: str, - conversation_id: str | None, - context: core.Context, - language: str | None = None, - agent_id: str | None = None, - device_id: str | None = None, -) -> ConversationResult: - """Process text and get intent.""" - agent = await _get_agent_manager(hass).async_get_agent(agent_id) - - if language is None: - language = hass.config.language - - _LOGGER.debug("Processing in %s: %s", language, text) - result = await agent.async_process( - ConversationInput( - text=text, - context=context, - conversation_id=conversation_id, - device_id=device_id, - language=language, - ) - ) - return result - - -class AgentManager: - """Class to manage conversation agents.""" - - default_agent: str = HOME_ASSISTANT_AGENT - _builtin_agent: AbstractConversationAgent | None = None - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the conversation agents.""" - self.hass = hass - self._agents: dict[str, AbstractConversationAgent] = {} - self._builtin_agent_init_lock = asyncio.Lock() - - def async_setup(self) -> None: - """Set up the conversation agents.""" - async_setup_default_agent(self.hass) - - async def async_get_agent( - self, agent_id: str | None = None - ) -> AbstractConversationAgent: - """Get the agent.""" - if agent_id is None: - agent_id = self.default_agent - - if agent_id == HOME_ASSISTANT_AGENT: - if self._builtin_agent is not None: - return self._builtin_agent - - async with self._builtin_agent_init_lock: - if self._builtin_agent is not None: - return self._builtin_agent - - self._builtin_agent = DefaultAgent(self.hass) - await self._builtin_agent.async_initialize( - self.hass.data.get(DATA_CONFIG) - ) - - return self._builtin_agent - - if agent_id not in self._agents: - raise ValueError(f"Agent {agent_id} not found") - - return self._agents[agent_id] - - @core.callback - def async_get_agent_info(self) -> list[AgentInfo]: - """List all agents.""" - agents: list[AgentInfo] = [ - AgentInfo( - id=HOME_ASSISTANT_AGENT, - name="Home Assistant", - ) - ] - for agent_id, agent in self._agents.items(): - config_entry = self.hass.config_entries.async_get_entry(agent_id) - - # Guard against potential bugs in conversation agents where the agent is not - # removed from the manager when the config entry is removed - if config_entry is None: - _LOGGER.warning( - "Conversation agent %s is still loaded after config entry removal", - agent, - ) - continue - - agents.append( - AgentInfo( - id=agent_id, - name=config_entry.title or config_entry.domain, - ) - ) - return agents - - @core.callback - def async_is_valid_agent_id(self, agent_id: str) -> bool: - """Check if the agent id is valid.""" - return agent_id in self._agents or agent_id == HOME_ASSISTANT_AGENT - - @core.callback - def async_set_agent(self, agent_id: str, agent: AbstractConversationAgent) -> None: - """Set the agent.""" - self._agents[agent_id] = agent - - @core.callback - def async_unset_agent(self, agent_id: str) -> None: - """Unset the agent.""" - self._agents.pop(agent_id, None) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py new file mode 100644 index 00000000000..9f31ccd6c62 --- /dev/null +++ b/homeassistant/components/conversation/agent_manager.py @@ -0,0 +1,151 @@ +"""Agent foundation for conversation integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.core import Context, HomeAssistant, async_get_hass, callback +from homeassistant.helpers import config_validation as cv, singleton +from homeassistant.helpers.entity_component import EntityComponent + +from .const import DOMAIN, HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT +from .default_agent import async_get_default_agent +from .entity import ConversationEntity +from .models import ( + AbstractConversationAgent, + AgentInfo, + ConversationInput, + ConversationResult, +) + +_LOGGER = logging.getLogger(__name__) + + +@singleton.singleton("conversation_agent") +@callback +def get_agent_manager(hass: HomeAssistant) -> AgentManager: + """Get the active agent.""" + return AgentManager(hass) + + +def agent_id_validator(value: Any) -> str: + """Validate agent ID.""" + hass = async_get_hass() + if async_get_agent(hass, cv.string(value)) is None: + raise vol.Invalid("invalid agent ID") + return value + + +@callback +def async_get_agent( + hass: HomeAssistant, agent_id: str | None = None +) -> AbstractConversationAgent | ConversationEntity | None: + """Get specified agent.""" + if agent_id is None or agent_id in (HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT): + return async_get_default_agent(hass) + + if "." in agent_id: + entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + return entity_component.get_entity(agent_id) + + manager = get_agent_manager(hass) + + if not manager.async_is_valid_agent_id(agent_id): + return None + + return manager.async_get_agent(agent_id) + + +async def async_converse( + hass: HomeAssistant, + text: str, + conversation_id: str | None, + context: Context, + language: str | None = None, + agent_id: str | None = None, + device_id: str | None = None, +) -> ConversationResult: + """Process text and get intent.""" + agent = async_get_agent(hass, agent_id) + + if agent is None: + raise ValueError(f"Agent {agent_id} not found") + + if isinstance(agent, ConversationEntity): + agent.async_set_context(context) + method = agent.internal_async_process + else: + method = agent.async_process + + if language is None: + language = hass.config.language + + _LOGGER.debug("Processing in %s: %s", language, text) + return await method( + ConversationInput( + text=text, + context=context, + conversation_id=conversation_id, + device_id=device_id, + language=language, + ) + ) + + +class AgentManager: + """Class to manage conversation agents.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the conversation agents.""" + self.hass = hass + self._agents: dict[str, AbstractConversationAgent] = {} + + @callback + def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None: + """Get the agent.""" + if agent_id not in self._agents: + raise ValueError(f"Agent {agent_id} not found") + + return self._agents[agent_id] + + @callback + def async_get_agent_info(self) -> list[AgentInfo]: + """List all agents.""" + agents: list[AgentInfo] = [] + for agent_id, agent in self._agents.items(): + config_entry = self.hass.config_entries.async_get_entry(agent_id) + + # Guard against potential bugs in conversation agents where the agent is not + # removed from the manager when the config entry is removed + if config_entry is None: + _LOGGER.warning( + "Conversation agent %s is still loaded after config entry removal", + agent, + ) + continue + + agents.append( + AgentInfo( + id=agent_id, + name=config_entry.title or config_entry.domain, + ) + ) + return agents + + @callback + def async_is_valid_agent_id(self, agent_id: str) -> bool: + """Check if the agent id is valid.""" + return agent_id in self._agents + + @callback + def async_set_agent(self, agent_id: str, agent: AbstractConversationAgent) -> None: + """Set the agent.""" + self._agents[agent_id] = agent + + @callback + def async_unset_agent(self, agent_id: str) -> None: + """Unset the agent.""" + self._agents.pop(agent_id, None) diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py index a8828fcc0e9..d20b6d96aa2 100644 --- a/homeassistant/components/conversation/const.py +++ b/homeassistant/components/conversation/const.py @@ -2,4 +2,5 @@ DOMAIN = "conversation" DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"} -HOME_ASSISTANT_AGENT = "homeassistant" +HOME_ASSISTANT_AGENT = "conversation.home_assistant" +OLD_HOME_ASSISTANT_AGENT = "homeassistant" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c0307c68908..121702115b9 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -24,7 +24,7 @@ from hassil.util import merge_dict from home_assistant_intents import ErrorKey, get_intents, get_languages import yaml -from homeassistant import core, setup +from homeassistant import core from homeassistant.components.homeassistant.exposed_entities import ( async_listen_entity_updates, async_should_expose, @@ -40,14 +40,13 @@ from homeassistant.helpers import ( template, translation, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_added_domain, -) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_state_added_domain from homeassistant.util.json import JsonObjectType, json_loads_object -from .agent import AbstractConversationAgent, ConversationInput, ConversationResult from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN +from .entity import ConversationEntity +from .models import ConversationInput, ConversationResult _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" @@ -60,6 +59,14 @@ TRIGGER_CALLBACK_TYPE = Callable[ METADATA_CUSTOM_SENTENCE = "hass_custom_sentence" METADATA_CUSTOM_FILE = "hass_custom_file" +DATA_DEFAULT_ENTITY = "conversation_default_entity" + + +@core.callback +def async_get_default_agent(hass: core.HomeAssistant) -> DefaultAgent: + """Get the default agent.""" + return hass.data[DATA_DEFAULT_ENTITY] + def json_load(fp: IO[str]) -> JsonObjectType: """Wrap json_loads for get_intents.""" @@ -109,15 +116,24 @@ def _get_language_variations(language: str) -> Iterable[str]: yield lang -@core.callback -def async_setup(hass: core.HomeAssistant) -> None: +async def async_setup_default_agent( + hass: core.HomeAssistant, + entity_component: EntityComponent[ConversationEntity], + config_intents: dict[str, Any], +) -> None: """Set up entity registry listener for the default agent.""" + entity = DefaultAgent(hass, config_intents) + await entity_component.async_add_entities([entity]) + hass.data[DATA_DEFAULT_ENTITY] = entity + entity_registry = er.async_get(hass) for entity_id in entity_registry.entities: async_should_expose(hass, DOMAIN, entity_id) @core.callback - def async_entity_state_listener(event: core.Event[EventStateChangedData]) -> None: + def async_entity_state_listener( + event: core.Event[core.EventStateChangedData], + ) -> None: """Set expose flag on new entities.""" async_should_expose(hass, DOMAIN, event.data["entity_id"]) @@ -131,60 +147,73 @@ def async_setup(hass: core.HomeAssistant) -> None: start.async_at_started(hass, async_hass_started) -class DefaultAgent(AbstractConversationAgent): +class DefaultAgent(ConversationEntity): """Default agent for conversation agent.""" - def __init__(self, hass: core.HomeAssistant) -> None: + _attr_name = "Home Assistant" + + def __init__( + self, hass: core.HomeAssistant, config_intents: dict[str, Any] + ) -> None: """Initialize the default agent.""" self.hass = hass self._lang_intents: dict[str, LanguageIntents] = {} self._lang_lock: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) # intent -> [sentences] - self._config_intents: dict[str, Any] = {} + self._config_intents: dict[str, Any] = config_intents self._slot_lists: dict[str, SlotList] | None = None # Sentences that will trigger a callback (skipping intent recognition) self._trigger_sentences: list[TriggerData] = [] self._trigger_intents: Intents | None = None + self._unsub_clear_slot_list: list[Callable[[], None]] | None = None @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" return get_languages() - async def async_initialize(self, config_intents: dict[str, Any] | None) -> None: - """Initialize the default agent.""" - if "intent" not in self.hass.config.components: - await setup.async_setup_component(self.hass, "intent", {}) + @core.callback + def _filter_entity_registry_changes( + self, event_data: er.EventEntityRegistryUpdatedData + ) -> bool: + """Filter entity registry changed events.""" + return event_data["action"] == "update" and any( + field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS + ) - # Intents from config may only contains sentences for HA config's language - if config_intents: - self._config_intents = config_intents + @core.callback + def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool: + """Filter state changed events.""" + return not event_data["old_state"] or not event_data["new_state"] - self.hass.bus.async_listen( - ar.EVENT_AREA_REGISTRY_UPDATED, - 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, - run_immediately=True, - ) - self.hass.bus.async_listen( - EVENT_STATE_CHANGED, - self._async_handle_state_changed, - run_immediately=True, - ) - async_listen_entity_updates( - self.hass, DOMAIN, self._async_exposed_entities_updated - ) + @core.callback + def _listen_clear_slot_list(self) -> None: + """Listen for changes that can invalidate slot list.""" + assert self._unsub_clear_slot_list is None + + self._unsub_clear_slot_list = [ + self.hass.bus.async_listen( + ar.EVENT_AREA_REGISTRY_UPDATED, + self._async_clear_slot_list, + ), + self.hass.bus.async_listen( + fr.EVENT_FLOOR_REGISTRY_UPDATED, + self._async_clear_slot_list, + ), + self.hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, + self._async_clear_slot_list, + event_filter=self._filter_entity_registry_changes, + ), + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, + self._async_clear_slot_list, + event_filter=self._filter_state_changes, + ), + async_listen_entity_updates(self.hass, DOMAIN, self._async_clear_slot_list), + ] async def async_recognize( self, user_input: ConversationInput @@ -209,7 +238,7 @@ class DefaultAgent(AbstractConversationAgent): slot_lists = self._make_slot_lists() intent_context = self._make_intent_context(user_input) - result = await self.hass.async_add_executor_job( + return await self.hass.async_add_executor_job( self._recognize, user_input, lang_intents, @@ -218,8 +247,6 @@ class DefaultAgent(AbstractConversationAgent): language, ) - return result - async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" language = user_input.language or self.hass.config.language @@ -237,20 +264,38 @@ class DefaultAgent(AbstractConversationAgent): for trigger_id, trigger_result in result.matched_triggers.items() ] - # Use last non-empty result as response. + # Use first non-empty result as response. # # There may be multiple copies of a trigger running when editing in # the UI, so it's critical that we filter out empty responses here. response_text: str | None = None + response_set_by_trigger = False for trigger_future in asyncio.as_completed(trigger_callbacks): - if trigger_response := await trigger_future: - response_text = trigger_response - break + trigger_response = await trigger_future + if trigger_response is None: + continue + + response_text = trigger_response + response_set_by_trigger = True + break # Convert to conversation result response = intent.IntentResponse(language=language) response.response_type = intent.IntentResponseType.ACTION_DONE - response.async_set_speech(response_text or "Done") + + if response_set_by_trigger: + # Response was explicitly set to empty + response_text = response_text or "" + elif not response_text: + # Use translated acknowledgment for pipeline language + translations = await translation.async_get_translations( + self.hass, language, DOMAIN, [DOMAIN] + ) + response_text = translations.get( + f"component.{DOMAIN}.agent.done", "Done" + ) + + response.async_set_speech(response_text) return ConversationResult(response=response) @@ -543,6 +588,9 @@ class DefaultAgent(AbstractConversationAgent): if lang_intents is None: # No intents loaded _LOGGER.warning("No intents were loaded for language: %s", language) + return + + self._make_slot_lists() async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: """Load all intents of a language with lock.""" @@ -702,40 +750,15 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents @core.callback - def _async_handle_area_floor_registry_changed( - self, - event: core.Event[ - ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData - ], - ) -> None: - """Clear area/floor list cache when the area registry has changed.""" + def _async_clear_slot_list(self, event: core.Event[Any] | None = None) -> None: + """Clear slot lists when a registry has changed.""" self._slot_lists = None + assert self._unsub_clear_slot_list is not None + for unsub in self._unsub_clear_slot_list: + unsub() + self._unsub_clear_slot_list = None @core.callback - def _async_handle_entity_registry_changed( - 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( - field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS - ): - return - self._slot_lists = None - - @core.callback - def _async_handle_state_changed( - 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"]: - return - self._slot_lists = None - - @core.callback - def _async_exposed_entities_updated(self) -> None: - """Handle updated preferences.""" - self._slot_lists = None - def _make_slot_lists(self) -> dict[str, SlotList]: """Create slot lists with areas and entity names/aliases.""" if self._slot_lists is not None: @@ -820,6 +843,7 @@ class DefaultAgent(AbstractConversationAgent): "floor": TextSlotList.from_tuples(floor_names, allow_template=False), } + self._listen_clear_slot_list() return self._slot_lists def _make_intent_context( @@ -871,8 +895,7 @@ class DefaultAgent(AbstractConversationAgent): # Force rebuild on next use self._trigger_intents = None - unregister = functools.partial(self._unregister_trigger, trigger_data) - return unregister + return functools.partial(self._unregister_trigger, trigger_data) def _rebuild_trigger_intents(self) -> None: """Rebuild the HassIL intents object from the current trigger sentences.""" diff --git a/homeassistant/components/conversation/entity.py b/homeassistant/components/conversation/entity.py new file mode 100644 index 00000000000..12dbea41344 --- /dev/null +++ b/homeassistant/components/conversation/entity.py @@ -0,0 +1,57 @@ +"""Entity for conversation integration.""" + +from abc import abstractmethod +from typing import Literal, final + +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util + +from .models import ConversationInput, ConversationResult + + +class ConversationEntity(RestoreEntity): + """Entity that supports conversations.""" + + _attr_should_poll = False + __last_activity: str | None = None + + @property + @final + def state(self) -> str | None: + """Return the state of the entity.""" + if self.__last_activity is None: + return None + return self.__last_activity + + async def async_internal_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if ( + state is not None + and state.state is not None + and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) + ): + self.__last_activity = state.state + + @final + async def internal_async_process( + self, user_input: ConversationInput + ) -> ConversationResult: + """Process a sentence.""" + self.__last_activity = dt_util.utcnow().isoformat() + self.async_write_ha_state() + return await self.async_process(user_input) + + @property + @abstractmethod + def supported_languages(self) -> list[str] | Literal["*"]: + """Return a list of supported languages.""" + + @abstractmethod + async def async_process(self, user_input: ConversationInput) -> ConversationResult: + """Process a sentence.""" + + async def async_prepare(self, language: str | None = None) -> None: + """Load intents for a language.""" diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py new file mode 100644 index 00000000000..beda7ba1550 --- /dev/null +++ b/homeassistant/components/conversation/http.py @@ -0,0 +1,357 @@ +"""HTTP endpoints for conversation integration.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +from aiohttp import web +from hassil.recognize import ( + MISSING_ENTITY, + RecognizeResult, + UnmatchedRangeEntity, + UnmatchedTextEntity, +) +import voluptuous as vol + +from homeassistant.components import http, websocket_api +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util import language as language_util + +from .agent_manager import ( + agent_id_validator, + async_converse, + async_get_agent, + get_agent_manager, +) +from .const import DOMAIN +from .default_agent import ( + METADATA_CUSTOM_FILE, + METADATA_CUSTOM_SENTENCE, + DefaultAgent, + SentenceTriggerResult, + async_get_default_agent, +) +from .entity import ConversationEntity +from .models import ConversationInput + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the HTTP API for the conversation integration.""" + hass.http.register_view(ConversationProcessView()) + websocket_api.async_register_command(hass, websocket_process) + websocket_api.async_register_command(hass, websocket_prepare) + websocket_api.async_register_command(hass, websocket_list_agents) + websocket_api.async_register_command(hass, websocket_hass_agent_debug) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/process", + vol.Required("text"): str, + vol.Optional("conversation_id"): vol.Any(str, None), + vol.Optional("language"): str, + vol.Optional("agent_id"): agent_id_validator, + } +) +@websocket_api.async_response +async def websocket_process( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Process text.""" + result = await async_converse( + hass=hass, + text=msg["text"], + conversation_id=msg.get("conversation_id"), + context=connection.context(msg), + language=msg.get("language"), + agent_id=msg.get("agent_id"), + ) + connection.send_result(msg["id"], result.as_dict()) + + +@websocket_api.websocket_command( + { + "type": "conversation/prepare", + vol.Optional("language"): str, + vol.Optional("agent_id"): agent_id_validator, + } +) +@websocket_api.async_response +async def websocket_prepare( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Reload intents.""" + agent = async_get_agent(hass, msg.get("agent_id")) + + if agent is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Agent not found" + ) + return + + await agent.async_prepare(msg.get("language")) + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/agent/list", + vol.Optional("language"): str, + vol.Optional("country"): str, + } +) +@websocket_api.async_response +async def websocket_list_agents( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """List conversation agents and, optionally, if they support a given language.""" + entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + + country = msg.get("country") + language = msg.get("language") + agents = [] + + for entity in entity_component.entities: + supported_languages = entity.supported_languages + if language and supported_languages != MATCH_ALL: + supported_languages = language_util.matches( + language, supported_languages, country + ) + + agents.append( + { + "id": entity.entity_id, + "name": entity.name or entity.entity_id, + "supported_languages": supported_languages, + } + ) + + manager = get_agent_manager(hass) + + for agent_info in manager.async_get_agent_info(): + agent = manager.async_get_agent(agent_info.id) + assert agent is not None + + supported_languages = agent.supported_languages + if language and supported_languages != MATCH_ALL: + supported_languages = language_util.matches( + language, supported_languages, country + ) + + agent_dict: dict[str, Any] = { + "id": agent_info.id, + "name": agent_info.name, + "supported_languages": supported_languages, + } + agents.append(agent_dict) + + connection.send_message(websocket_api.result_message(msg["id"], {"agents": agents})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/agent/homeassistant/debug", + vol.Required("sentences"): [str], + vol.Optional("language"): str, + vol.Optional("device_id"): vol.Any(str, None), + } +) +@websocket_api.async_response +async def websocket_hass_agent_debug( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return intents that would be matched by the default agent for a list of sentences.""" + agent = async_get_default_agent(hass) + assert isinstance(agent, DefaultAgent) + results = [ + await agent.async_recognize( + ConversationInput( + text=sentence, + context=connection.context(msg), + conversation_id=None, + device_id=msg.get("device_id"), + language=msg.get("language", hass.config.language), + ) + ) + for sentence in msg["sentences"] + ] + + # Return results for each sentence in the same order as the input. + result_dicts: list[dict[str, Any] | None] = [] + for result in results: + result_dict: dict[str, Any] | None = None + if isinstance(result, SentenceTriggerResult): + result_dict = { + # Matched a user-defined sentence trigger. + # We can't provide the response here without executing the + # trigger. + "match": True, + "source": "trigger", + "sentence_template": result.sentence_template or "", + } + elif isinstance(result, RecognizeResult): + successful_match = not result.unmatched_entities + result_dict = { + # Name of the matching intent (or the closest) + "intent": { + "name": result.intent.name, + }, + # Slot values that would be received by the intent + "slots": { # direct access to values + entity_key: entity.text or entity.value + for entity_key, entity in result.entities.items() + }, + # Extra slot details, such as the originally matched text + "details": { + entity_key: { + "name": entity.name, + "value": entity.value, + "text": entity.text, + } + for entity_key, entity in result.entities.items() + }, + # Entities/areas/etc. that would be targeted + "targets": {}, + # True if match was successful + "match": successful_match, + # Text of the sentence template that matched (or was closest) + "sentence_template": "", + # When match is incomplete, this will contain the best slot guesses + "unmatched_slots": _get_unmatched_slots(result), + } + + if successful_match: + result_dict["targets"] = { + state.entity_id: {"matched": is_matched} + for state, is_matched in _get_debug_targets(hass, result) + } + + if result.intent_sentence is not None: + result_dict["sentence_template"] = result.intent_sentence.text + + # Inspect metadata to determine if this matched a custom sentence + if result.intent_metadata and result.intent_metadata.get( + METADATA_CUSTOM_SENTENCE + ): + result_dict["source"] = "custom" + result_dict["file"] = result.intent_metadata.get(METADATA_CUSTOM_FILE) + else: + result_dict["source"] = "builtin" + + result_dicts.append(result_dict) + + connection.send_result(msg["id"], {"results": result_dicts}) + + +def _get_debug_targets( + hass: HomeAssistant, + result: RecognizeResult, +) -> Iterable[tuple[State, bool]]: + """Yield state/is_matched pairs for a hassil recognition.""" + entities = result.entities + + name: str | None = None + area_name: str | None = None + domains: set[str] | None = None + device_classes: set[str] | None = None + state_names: set[str] | None = None + + if "name" in entities: + name = str(entities["name"].value) + + if "area" in entities: + area_name = str(entities["area"].value) + + if "domain" in entities: + domains = set(cv.ensure_list(entities["domain"].value)) + + if "device_class" in entities: + device_classes = set(cv.ensure_list(entities["device_class"].value)) + + if "state" in entities: + # HassGetState only + state_names = set(cv.ensure_list(entities["state"].value)) + + if ( + (name is None) + and (area_name is None) + and (not domains) + and (not device_classes) + and (not state_names) + ): + # Avoid "matching" all entities when there is no filter + return + + states = intent.async_match_states( + hass, + name=name, + area_name=area_name, + domains=domains, + device_classes=device_classes, + ) + + for state in states: + # For queries, a target is "matched" based on its state + is_matched = (state_names is None) or (state.state in state_names) + yield state, is_matched + + +def _get_unmatched_slots( + result: RecognizeResult, +) -> dict[str, str | int]: + """Return a dict of unmatched text/range slot entities.""" + unmatched_slots: dict[str, str | int] = {} + for entity in result.unmatched_entities_list: + if isinstance(entity, UnmatchedTextEntity): + if entity.text == MISSING_ENTITY: + # Don't report since these are just missing context + # slots. + continue + + unmatched_slots[entity.name] = entity.text + elif isinstance(entity, UnmatchedRangeEntity): + unmatched_slots[entity.name] = entity.value + + return unmatched_slots + + +class ConversationProcessView(http.HomeAssistantView): + """View to process text.""" + + url = "/api/conversation/process" + name = "api:conversation:process" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("text"): str, + vol.Optional("conversation_id"): str, + vol.Optional("language"): str, + vol.Optional("agent_id"): agent_id_validator, + } + ) + ) + async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: + """Send a request for processing.""" + hass = request.app[http.KEY_HASS] + + result = await async_converse( + hass, + text=data["text"], + conversation_id=data.get("conversation_id"), + context=self.context(request), + language=data.get("language"), + agent_id=data.get("agent_id"), + ) + + return self.json(result.as_dict()) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 612e9b25c06..82e2adca680 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,10 +2,9 @@ "domain": "conversation", "name": "Conversation", "codeowners": ["@home-assistant/core", "@synesthesiam"], - "dependencies": ["http"], + "dependencies": ["http", "intent"], "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", - "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.24"] } diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/models.py similarity index 92% rename from homeassistant/components/conversation/agent.py rename to homeassistant/components/conversation/models.py index 22b3437907c..3fd24152698 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/models.py @@ -10,6 +10,14 @@ from homeassistant.core import Context from homeassistant.helpers import intent +@dataclass(frozen=True) +class AgentInfo: + """Container for conversation agent info.""" + + id: str + name: str + + @dataclass(slots=True) class ConversationInput: """User input to be processed.""" diff --git a/homeassistant/components/conversation/strings.json b/homeassistant/components/conversation/strings.json index 3150623ba65..e3c3aa5af20 100644 --- a/homeassistant/components/conversation/strings.json +++ b/homeassistant/components/conversation/strings.json @@ -37,5 +37,10 @@ } } } + }, + "conversation": { + "agent": { + "done": "Done" + } } } diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index 0fadc458352..0a4cbfcb7e5 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -14,9 +14,8 @@ from homeassistant.helpers.script import ScriptRunResult from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import UNDEFINED, ConfigType -from . import HOME_ASSISTANT_AGENT, _get_agent_manager from .const import DOMAIN -from .default_agent import DefaultAgent +from .default_agent import DefaultAgent, async_get_default_agent def has_no_punctuation(value: list[str]) -> list[str]: @@ -111,7 +110,7 @@ async def async_attach_trigger( # two trigger copies for who will provide a response. return None - default_agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) + default_agent = async_get_default_agent(hass) assert isinstance(default_agent, DefaultAgent) return default_agent.register_trigger(sentences, call_action) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 1eac6844703..5c7139d6290 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -6,8 +6,9 @@ from collections.abc import Callable from datetime import timedelta from enum import IntFlag, StrEnum import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, final +from typing import Any, ParamSpec, TypeVar, final import voluptuous as vol @@ -46,11 +47,6 @@ from homeassistant.loader import bind_hass from . import group as group_pre_import # noqa: F401 -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) DOMAIN = "cover" @@ -484,15 +480,30 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def _get_toggle_function( self, fns: dict[str, Callable[_P, _R]] ) -> Callable[_P, _R]: + # If we are opening or closing and we support stopping, then we should stop if self.supported_features & CoverEntityFeature.STOP and ( self.is_closing or self.is_opening ): return fns["stop"] - if self.is_closed: + + # If we are fully closed or in the process of closing, then we should open + if self.is_closed or self.is_closing: return fns["open"] - if self._cover_is_last_toggle_direction_open: + + # If we are fully open or in the process of opening, then we should close + if self.current_cover_position == 100 or self.is_opening: return fns["close"] - return fns["open"] + + # We are any of: + # * fully open but do not report `current_cover_position` + # * stopped partially open + # * either opening or closing, but do not report them + # If we previously reported opening/closing, we should move in the opposite direction. + # Otherwise, we must assume we are (partially) open and should always close. + # Note: _cover_is_last_toggle_direction_open will always remain True if we never report opening/closing. + return ( + fns["close"] if self._cover_is_last_toggle_direction_open else fns["open"] + ) # These can be removed if no deprecated constant are in this module anymore diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 8978028641d..9b1ebbb1ed8 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -64,10 +64,9 @@ class CPPMDeviceScanner(DeviceScanner): def get_device_name(self, device): """Retrieve device name.""" - name = next( + return next( (result["name"] for result in self.results if result["mac"] == device), None ) - return name def get_cppm_data(self): """Retrieve data from Aruba Clearpass and return parsed result.""" diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index f0b62e95b1f..6f1196c7721 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -97,9 +97,7 @@ async def daikin_api_setup( _LOGGER.error("Unexpected error creating device %s", host) return None - api = DaikinApi(device) - - return api + return DaikinApi(device) class DaikinApi: diff --git a/homeassistant/components/date/__init__.py b/homeassistant/components/date/__init__.py index 3cb6ad3a77d..ddd85ffbf06 100644 --- a/homeassistant/components/date/__init__.py +++ b/homeassistant/components/date/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import date, timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -22,12 +23,6 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py index 420cf27b5aa..b1be0a0d08d 100644 --- a/homeassistant/components/datetime/__init__.py +++ b/homeassistant/components/datetime/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -22,11 +23,6 @@ from homeassistant.util import dt as dt_util from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index eaa89c6eb9c..02f6ada8fc8 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -33,8 +33,6 @@ from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .hub import DeconzHub -_SensorDeviceT = TypeVar("_SensorDeviceT", bound=PydeconzSensorBase) - ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" ATTR_VIBRATIONSTRENGTH = "vibrationstrength" diff --git a/homeassistant/components/deconz/hub/api.py b/homeassistant/components/deconz/hub/api.py index 71551ead6e1..916c34672d8 100644 --- a/homeassistant/components/deconz/hub/api.py +++ b/homeassistant/components/deconz/hub/api.py @@ -26,7 +26,6 @@ async def get_deconz_api( 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) @@ -35,3 +34,4 @@ async def get_deconz_api( except (TimeoutError, errors.RequestError, errors.ResponseError) as err: LOGGER.error("Error connecting to deCONZ gateway at %s", config.host) raise CannotConnect from err + return api diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 91f36bb871e..233f9c3f570 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -160,8 +160,9 @@ async def async_remove_orphaned_entries_service(hub: DeconzHub) -> None: entities_to_be_removed = [] devices_to_be_removed = [ entry.id - for entry in device_registry.devices.values() - if hub.config_entry.entry_id in entry.config_entries + for entry in device_registry.devices.get_devices_for_config_entry_id( + hub.config_entry.entry_id + ) ] # Don't remove the Gateway host entry diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 6b3c177b90d..c3dd25609fe 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -63,5 +63,5 @@ class DelugeDataUpdateCoordinator( "Credentials for Deluge client are not valid" ) from ex LOGGER.error("Unknown error connecting to Deluge: %s", ex) - raise ex + raise return data diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 6fa7e0d973b..738f6af38dd 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -38,6 +38,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.LIGHT, Platform.LOCK, Platform.MEDIA_PLAYER, + Platform.NOTIFY, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, @@ -55,7 +56,6 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ COMPONENTS_WITH_DEMO_PLATFORM = [ Platform.TTS, Platform.MAILBOX, - Platform.NOTIFY, Platform.IMAGE_PROCESSING, Platform.DEVICE_TRACKER, ] diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index c6a9483b328..94999d26d10 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -1,38 +1,44 @@ -"""Demo notification service.""" +"""Demo notification entity.""" from __future__ import annotations -from typing import Any - -from homeassistant.components.notify import BaseNotificationService +from homeassistant.components.notify import DOMAIN, NotifyEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback EVENT_NOTIFY = "notify" -def get_service( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> BaseNotificationService: - """Get the demo notification service.""" - return DemoNotificationService(hass) + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the demo entity platform.""" + async_add_entities([DemoNotifyEntity(unique_id="notify", device_name="Notifier")]) -class DemoNotificationService(BaseNotificationService): - """Implement demo notification service.""" +class DemoNotifyEntity(NotifyEntity): + """Implement demo notification platform.""" - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the service.""" - self.hass = hass + _attr_has_entity_name = True + _attr_name = None - @property - def targets(self) -> dict[str, str]: - """Return a dictionary of registered targets.""" - return {"test target name": "test target id"} + def __init__( + self, + unique_id: str, + device_name: str, + ) -> None: + """Initialize the Demo button entity.""" + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) - def send_message(self, message: str = "", **kwargs: Any) -> None: + async def async_send_message(self, message: str) -> None: """Send a message to a user.""" - kwargs["message"] = message - self.hass.bus.fire(EVENT_NOTIFY, kwargs) + event_notitifcation = {"message": message} + self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation) diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 4281ca9cc59..0c61faae00e 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -149,7 +149,7 @@ class DemoSensor(SensorEntity): self, unique_id: str, device_name: str | None, - state: float | int | str | None, + state: float | str | None, device_class: SensorDeviceClass, state_class: SensorStateClass | None, unit_of_measurement: str | None, diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 2f9b96d9471..25e4cc0119c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -232,12 +232,10 @@ def async_log_errors( func.__name__, err, ) - except DenonAvrError as err: + except DenonAvrError: available = False _LOGGER.exception( - "Error %s occurred in method %s for Denon AVR receiver", - err, - func.__name__, + "Error occurred in method %s for Denon AVR receiver", func.__name__ ) finally: if available and not self.available: diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index ea343288c9c..d5a83035ed5 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -27,10 +27,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 861a634eda7..6781b9afaf7 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,6 +1,7 @@ """Support to turn on lights based on the states.""" from datetime import timedelta +from functools import partial import logging import voluptuous as vol @@ -27,11 +28,11 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_utc_time, - async_track_state_change, + async_track_state_change_event, ) from homeassistant.helpers.sun import get_astral_event_next, is_up from homeassistant.helpers.typing import ConfigType @@ -195,8 +196,20 @@ async def activate_automation( # noqa: C901 schedule_light_turn_on(None) @callback - def check_light_on_dev_state_change(entity, old_state, new_state): + def check_light_on_dev_state_change( + from_state: str, to_state: str, event: Event[EventStateChangedData] + ) -> None: """Handle tracked device state changes.""" + event_data = event.data + if ( + (old_state := event_data["old_state"]) is None + or (new_state := event_data["new_state"]) is None + or old_state.state != from_state + or new_state.state != to_state + ): + return + + entity = event_data["entity_id"] lights_are_on = any_light_on() light_needed = not (lights_are_on or is_up(hass)) @@ -237,12 +250,10 @@ async def activate_automation( # noqa: C901 # will all the following then, break. break - async_track_state_change( + async_track_state_change_event( hass, device_entity_ids, - check_light_on_dev_state_change, - STATE_NOT_HOME, - STATE_HOME, + partial(check_light_on_dev_state_change, STATE_NOT_HOME, STATE_HOME), ) if disable_turn_off: @@ -266,12 +277,10 @@ async def activate_automation( # noqa: C901 ) ) - async_track_state_change( + async_track_state_change_event( hass, device_entity_ids, - turn_off_lights_when_all_leave, - STATE_HOME, - STATE_NOT_HOME, + partial(turn_off_lights_when_all_leave, STATE_HOME, STATE_NOT_HOME), ) return diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index a1c1961dc43..0372dff3a86 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -18,7 +18,10 @@ from homeassistant.const import ( ) 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.device_registry import ( + DeviceInfo, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -51,28 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) component.register_shutdown() - # Clean up old devices created by device tracker entities in the past. - # Can be removed after 2022.6 - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - - devices_with_trackers = set() - devices_with_non_trackers = set() - - for entity in ent_reg.entities.values(): - if entity.device_id is None: - continue - - if entity.domain == DOMAIN: - devices_with_trackers.add(entity.device_id) - else: - devices_with_non_trackers.add(entity.device_id) - - for device_id in devices_with_trackers - devices_with_non_trackers: - for entity in er.async_entries_for_device(ent_reg, device_id, True): - ent_reg.async_update_entity(entity.entity_id, device_id=None) - dev_reg.async_remove_device(device_id) - return await component.async_setup_entry(entry) @@ -123,7 +104,7 @@ def _async_register_mac( data = hass.data[data_key] = {mac: (domain, unique_id)} @callback - def handle_device_event(ev: Event) -> None: + def handle_device_event(ev: Event[EventDeviceRegistryUpdatedData]) -> None: """Enable the online status entity for the mac of a newly created device.""" # Only for new devices if ev.data["action"] != "create": @@ -178,9 +159,7 @@ def _async_register_mac( # Enable entity ent_reg.async_update_entity(entity_id, disabled_by=None) - hass.bus.async_listen( - dr.EVENT_DEVICE_REGISTRY_UPDATED, handle_device_event, run_immediately=True - ) + hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, handle_device_event) class BaseTrackerEntity(Entity): diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 91cf35f43bd..dfeed98f320 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine, Sequence from datetime import datetime, timedelta +from functools import cached_property import hashlib from types import ModuleType from typing import Any, Final, Protocol, final @@ -13,7 +14,6 @@ import attr 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 ( @@ -281,9 +281,7 @@ async def _async_setup_integration( """ cancel_update_stale() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) @attr.s @@ -524,7 +522,7 @@ def async_setup_scanner_platform( ] kwargs["gps_accuracy"] = 0 - hass.async_create_task(async_see_device(**kwargs)) + hass.async_create_task(async_see_device(**kwargs), eager_start=True) cancel_legacy_scan = async_track_time_interval( hass, @@ -532,7 +530,7 @@ def async_setup_scanner_platform( interval, name=f"device_tracker {platform} legacy scan", ) - hass.async_create_task(async_device_tracker_scan(None)) + hass.async_create_task(async_device_tracker_scan(None), eager_start=True) @callback def _on_hass_stop(_: Event) -> None: @@ -558,8 +556,7 @@ async def get_tracker(hass: HomeAssistant, config: ConfigType) -> DeviceTracker: track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) devices = await async_load_config(yaml_path, hass, consider_home) - tracker = DeviceTracker(hass, consider_home, track_new, defaults, devices) - return tracker + return DeviceTracker(hass, consider_home, track_new, defaults, devices) class DeviceTracker: @@ -722,7 +719,8 @@ class DeviceTracker: self.hass.async_create_task( self.async_update_config( self.hass.config.path(YAML_DEVICES), dev_id, device - ) + ), + eager_start=True, ) async def async_update_config(self, path: str, dev_id: str, device: Device) -> None: @@ -743,7 +741,9 @@ class DeviceTracker: """ for device in self.devices.values(): if (device.track and device.last_update_home) and device.stale(now): - self.hass.async_create_task(device.async_update_ha_state(True)) + self.hass.async_create_task( + device.async_update_ha_state(True), eager_start=True + ) async def async_setup_tracked_device(self) -> None: """Set up all not exists tracked devices. diff --git a/homeassistant/components/devolo_home_control/diagnostics.py b/homeassistant/components/devolo_home_control/diagnostics.py index 753d04db0a3..33652f8e0bc 100644 --- a/homeassistant/components/devolo_home_control/diagnostics.py +++ b/homeassistant/components/devolo_home_control/diagnostics.py @@ -41,9 +41,7 @@ async def async_get_config_entry_diagnostics( for gateway in gateways ] - diag_data = { + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": device_info, } - - return diag_data diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index 9d86b127d77..97348c5c43c 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -50,7 +50,7 @@ }, "image": { "image_guest_wifi": { - "name": "Guest Wifi credentials as QR code" + "name": "Guest Wi-Fi credentials as QR code" } }, "sensor": { @@ -58,10 +58,10 @@ "name": "Connected PLC devices" }, "connected_wifi_clients": { - "name": "Connected Wifi clients" + "name": "Connected Wi-Fi clients" }, "neighboring_wifi_networks": { - "name": "Neighboring Wifi networks" + "name": "Neighboring Wi-Fi networks" }, "plc_rx_rate": { "name": "PLC downlink PHY rate" @@ -72,7 +72,7 @@ }, "switch": { "switch_guest_wifi": { - "name": "Enable guest Wifi" + "name": "Enable guest Wi-Fi" }, "switch_leds": { "name": "Enable LEDs" diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 050bc7a74b2..b4d06b6e276 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -37,7 +37,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, STATE_HOME, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.device_registry import ( @@ -48,7 +54,6 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( - EventStateChangedData, async_track_state_added_domain, async_track_time_interval, ) @@ -157,13 +162,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for watcher in watchers: watcher.async_stop() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_initialize, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) return True diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 0d77b997e82..b8abd0a9919 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.0.0", - "aiodiscover==2.0.0", + "aiodiscover==2.1.0", "cached_ipaddress==0.3.0" ] } diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index 5ada7713c33..6c70e0dc110 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -174,6 +174,7 @@ async def _async_get_json_file_response( all_custom_components = await async_get_custom_components(hass) for cc_domain, cc_obj in all_custom_components.items(): custom_components[cc_domain] = { + "documentation": cc_obj.documentation, "version": cc_obj.version, "requirements": cc_obj.requirements, } diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py index a2747c1d803..a25a86cab3a 100644 --- a/homeassistant/components/discord/config_flow.py +++ b/homeassistant/components/discord/config_flow.py @@ -89,8 +89,8 @@ async def _async_try_connect(token: str) -> tuple[str | None, nextcord.AppInfo | return "invalid_auth", None except (ClientConnectorError, nextcord.HTTPException, nextcord.NotFound): return "cannot_connect", None - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown", None await discord_bot.close() return None, info diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py index 52937d26b7d..4613aeb9cef 100644 --- a/homeassistant/components/dlink/config_flow.py +++ b/homeassistant/components/dlink/config_flow.py @@ -121,8 +121,8 @@ class DLinkFlowHandler(ConfigFlow, domain=DOMAIN): user_input[CONF_USERNAME], user_input[CONF_USE_LEGACY_PROTOCOL], ) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown" if not smartplug.authenticated and smartplug.use_legacy_protocol: return "cannot_connect" diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index a37caa6700c..36bfe4fb391 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -51,13 +51,11 @@ class SmartPlugSwitch(DLinkEntity, SwitchEntity): except ValueError: total_consumption = None - attrs = { + return { ATTR_TOTAL_CONSUMPTION: total_consumption, ATTR_TEMPERATURE: temperature, } - return attrs - @property def is_on(self) -> bool: """Return true if switch is on.""" diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 9d95ba3883e..837bfc456d8 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -339,11 +339,7 @@ class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries(include_ignore=False) } - discoveries = [ - disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids - ] - - return discoveries + return [disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids] class DlnaDmrOptionsFlowHandler(OptionsFlow): diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 41fa49f1a94..ebbab957700 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "after_dependencies": ["media_source"], - "codeowners": ["@StevenLooman", "@chishm"], + "codeowners": ["@chishm"], "config_flow": true, "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", diff --git a/homeassistant/components/dlna_dms/config_flow.py b/homeassistant/components/dlna_dms/config_flow.py index 480f45ee95b..b50dc7ff227 100644 --- a/homeassistant/components/dlna_dms/config_flow.py +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -179,8 +179,4 @@ class DlnaDmsFlowHandler(ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries(include_ignore=False) } - discoveries = [ - disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids - ] - - return discoveries + return [disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids] diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index aaa55e3ad3e..2312c7d2e3d 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -7,6 +7,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import StrEnum import functools +from functools import cached_property from typing import Any, TypeVar, cast from async_upnp_client.aiohttp import AiohttpSessionRequester @@ -17,7 +18,6 @@ from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, U from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite -from homeassistant.backports.functools import cached_property from homeassistant.components import ssdp from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable @@ -516,7 +516,7 @@ class DmsDeviceSource: if isinstance(child, didl_lite.DidlObject) ] - media_source = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=self._make_identifier(Action.SEARCH, query), media_class=MediaClass.DIRECTORY, @@ -527,8 +527,6 @@ class DmsDeviceSource: children=children, ) - return media_source - def _didl_to_play_media(self, item: didl_lite.DidlObject) -> DidlPlayMedia: """Return the first playable resource from a DIDL-Lite object.""" assert self._device @@ -583,7 +581,7 @@ class DmsDeviceSource: mime_type = _resource_mime_type(item.res[0]) if item.res else None media_content_type = mime_type or item.upnp_class - media_source = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=self._make_identifier(Action.OBJECT, item.id), media_class=MEDIA_CLASS_MAP.get(item.upnp_class, ""), @@ -595,8 +593,6 @@ class DmsDeviceSource: thumbnail=self._didl_thumbnail_url(item), ) - return media_source - def _didl_thumbnail_url(self, item: didl_lite.DidlObject) -> str | None: """Return absolute URL of a thumbnail for a DIDL-Lite object. diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 17c0677e4d9..d25459b95b7 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.1.1"] + "requirements": ["aiodns==3.2.0"] } diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 48404e6dbee..ce7b36f2280 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -139,10 +139,10 @@ class Dominos: """Update the shared closest store (if open).""" try: self.closest_store = self.address.closest_store() - return True except StoreException: self.closest_store = None return False + return True def get_menu(self): """Return the products from the closest stores menu.""" diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index ddc8ae0bdc5..961a3287799 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -107,8 +107,6 @@ class DoorBirdCamera(DoorBirdEntity, Camera): response = await websession.get(self._url) self._last_image = await response.read() - self._last_update = now - return self._last_image except TimeoutError: _LOGGER.error("DoorBird %s: Camera image timed out", self.name) return self._last_image @@ -118,6 +116,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera): ) return self._last_image + self._last_update = now + return self._last_image + async def async_added_to_hass(self) -> None: """Subscribe to events.""" await super().async_added_to_hass() diff --git a/homeassistant/components/dormakaba_dkey/strings.json b/homeassistant/components/dormakaba_dkey/strings.json index 480f021b126..1fdc7cb359f 100644 --- a/homeassistant/components/dormakaba_dkey/strings.json +++ b/homeassistant/components/dormakaba_dkey/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 60e8351cc24..e89fd4361a5 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -74,10 +74,11 @@ class DovadoData: if not self.state: return False self.state.update(connected=self.state.get("modem status") == "CONNECTED") - _LOGGER.debug("Received: %s", self.state) - return True except OSError as error: _LOGGER.warning("Could not contact the router: %s", error) + return None + _LOGGER.debug("Received: %s", self.state) + return True @property def client(self): diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 94b33f4e93f..e7191e055a6 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -25,14 +25,11 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): """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" + errors["base"] = "directory_does_not_exist" else: return self.async_create_entry(title=DEFAULT_NAME, data=user_input) @@ -48,9 +45,6 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - try: await self._validate_input(user_input) except DirectoryDoesNotExist: diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 876404be889..85434069b87 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@erwindouna"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/downloader", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 4cadabf96c6..cf962bd9713 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -6,7 +6,7 @@ } }, "error": { - "cannot_connect": "The directory could not be reached. Please check your settings." + "directory_does_not_exist": "The directory could not be reached. Please check your settings." }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" diff --git a/homeassistant/components/dsmr_reader/diagnostics.py b/homeassistant/components/dsmr_reader/diagnostics.py new file mode 100644 index 00000000000..554d90cc5dd --- /dev/null +++ b/homeassistant/components/dsmr_reader/diagnostics.py @@ -0,0 +1,27 @@ +"""Diagnostics support for DSMR Reader.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for the config entry.""" + ent_reg = er.async_get(hass) + entities = [ + entity.entity_id + for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id) + ] + + entity_states = {entity: hass.states.get(entity) for entity in entities} + + return { + "entry": entry.as_dict(), + "entities": entity_states, + } diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index 275d47d15ca..9cf73a90a73 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -2,23 +2,16 @@ from __future__ import annotations -from dwdwfsapi import DwdWeatherWarningsAPI - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import CONF_REGION_IDENTIFIER, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS from .coordinator import DwdWeatherWarningsCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - region_identifier: str = entry.data[CONF_REGION_IDENTIFIER] - - # Initialize the API and coordinator. - api = await hass.async_add_executor_job(DwdWeatherWarningsAPI, region_identifier) - coordinator = DwdWeatherWarningsCoordinator(hass, api) - + coordinator = DwdWeatherWarningsCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/dwd_weather_warnings/config_flow.py b/homeassistant/components/dwd_weather_warnings/config_flow.py index 5076dbae187..f148f4e05ac 100644 --- a/homeassistant/components/dwd_weather_warnings/config_flow.py +++ b/homeassistant/components/dwd_weather_warnings/config_flow.py @@ -8,9 +8,15 @@ from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig -from .const import CONF_REGION_IDENTIFIER, DOMAIN +from .const import CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, DOMAIN +from .exceptions import EntityNotFoundError +from .util import get_position_data + +EXCLUSIVE_OPTIONS = (CONF_REGION_IDENTIFIER, CONF_REGION_DEVICE_TRACKER) class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): @@ -25,27 +31,70 @@ class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict = {} if user_input is not None: - region_identifier = user_input[CONF_REGION_IDENTIFIER] + # Check, if either CONF_REGION_IDENTIFIER or CONF_GPS_TRACKER has been set. + if all(k not in user_input for k in EXCLUSIVE_OPTIONS): + errors["base"] = "no_identifier" + elif all(k in user_input for k in EXCLUSIVE_OPTIONS): + errors["base"] = "ambiguous_identifier" + elif CONF_REGION_IDENTIFIER in user_input: + # Validate region identifier using the API + identifier = user_input[CONF_REGION_IDENTIFIER] - # Validate region identifier using the API - if not await self.hass.async_add_executor_job( - DwdWeatherWarningsAPI, region_identifier - ): - errors["base"] = "invalid_identifier" + if not await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, identifier + ): + errors["base"] = "invalid_identifier" - if not errors: - # Set the unique ID for this config entry. - await self.async_set_unique_id(region_identifier) - self._abort_if_unique_id_configured() + if not errors: + # Set the unique ID for this config entry. + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=region_identifier, data=user_input) + return self.async_create_entry(title=identifier, data=user_input) + else: # CONF_REGION_DEVICE_TRACKER + device_tracker = user_input[CONF_REGION_DEVICE_TRACKER] + registry = er.async_get(self.hass) + entity_entry = registry.async_get(device_tracker) + + if entity_entry is None: + errors["base"] = "entity_not_found" + else: + try: + position = get_position_data(self.hass, entity_entry.id) + except EntityNotFoundError: + errors["base"] = "entity_not_found" + except AttributeError: + errors["base"] = "attribute_not_found" + else: + # Validate position using the API + if not await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, position + ): + errors["base"] = "invalid_identifier" + + # Position is valid here, because the API call was successful. + if not errors and position is not None and entity_entry is not None: + # Set the unique ID for this config entry. + await self.async_set_unique_id(entity_entry.id) + self._abort_if_unique_id_configured() + + # Replace entity ID with registry ID for more stability. + user_input[CONF_REGION_DEVICE_TRACKER] = entity_entry.id + + return self.async_create_entry( + title=device_tracker.removeprefix("device_tracker."), + data=user_input, + ) return self.async_show_form( step_id="user", errors=errors, data_schema=vol.Schema( { - vol.Required(CONF_REGION_IDENTIFIER): cv.string, + vol.Optional(CONF_REGION_IDENTIFIER): cv.string, + vol.Optional(CONF_REGION_DEVICE_TRACKER): EntitySelector( + EntitySelectorConfig(domain="device_tracker") + ), } ), ) diff --git a/homeassistant/components/dwd_weather_warnings/const.py b/homeassistant/components/dwd_weather_warnings/const.py index 75969dee119..4f0a6767660 100644 --- a/homeassistant/components/dwd_weather_warnings/const.py +++ b/homeassistant/components/dwd_weather_warnings/const.py @@ -14,6 +14,7 @@ DOMAIN: Final = "dwd_weather_warnings" CONF_REGION_NAME: Final = "region_name" CONF_REGION_IDENTIFIER: Final = "region_identifier" +CONF_REGION_DEVICE_TRACKER: Final = "region_device_tracker" ATTR_REGION_NAME: Final = "region_name" ATTR_REGION_ID: Final = "region_id" diff --git a/homeassistant/components/dwd_weather_warnings/coordinator.py b/homeassistant/components/dwd_weather_warnings/coordinator.py index a1232697130..465a7c09750 100644 --- a/homeassistant/components/dwd_weather_warnings/coordinator.py +++ b/homeassistant/components/dwd_weather_warnings/coordinator.py @@ -4,23 +4,79 @@ from __future__ import annotations from dwdwfsapi import DwdWeatherWarningsAPI +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import location -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import ( + CONF_REGION_DEVICE_TRACKER, + CONF_REGION_IDENTIFIER, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) +from .exceptions import EntityNotFoundError +from .util import get_position_data class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]): """Custom coordinator for the dwd_weather_warnings integration.""" - def __init__(self, hass: HomeAssistant, api: DwdWeatherWarningsAPI) -> None: + config_entry: ConfigEntry + api: DwdWeatherWarningsAPI + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the dwd_weather_warnings coordinator.""" super().__init__( hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL ) - self.api = api + self._device_tracker = None + self._previous_position = None + + async def async_config_entry_first_refresh(self) -> None: + """Perform first refresh.""" + if region_identifier := self.config_entry.data.get(CONF_REGION_IDENTIFIER): + self.api = await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, region_identifier + ) + else: + self._device_tracker = self.config_entry.data.get( + CONF_REGION_DEVICE_TRACKER + ) + + await super().async_config_entry_first_refresh() async def _async_update_data(self) -> None: """Get the latest data from the DWD Weather Warnings API.""" - await self.hass.async_add_executor_job(self.api.update) + if self._device_tracker: + try: + position = get_position_data(self.hass, self._device_tracker) + except (EntityNotFoundError, AttributeError) as err: + raise UpdateFailed(f"Error fetching position: {repr(err)}") from err + + distance = None + if self._previous_position is not None: + distance = location.distance( + self._previous_position[0], + self._previous_position[1], + position[0], + position[1], + ) + + if distance is None or distance > 50: + # Only create a new object on the first update + # or when the distance to the previous position + # changes by more than 50 meters (to take GPS + # inaccuracy into account). + self.api = await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, position + ) + else: + # Otherwise update the API to check for new warnings. + await self.hass.async_add_executor_job(self.api.update) + + self._previous_position = position + else: + await self.hass.async_add_executor_job(self.api.update) diff --git a/homeassistant/components/dwd_weather_warnings/exceptions.py b/homeassistant/components/dwd_weather_warnings/exceptions.py new file mode 100644 index 00000000000..cd61cfa6bae --- /dev/null +++ b/homeassistant/components/dwd_weather_warnings/exceptions.py @@ -0,0 +1,7 @@ +"""Exceptions for the dwd_weather_warnings integration.""" + +from homeassistant.exceptions import HomeAssistantError + + +class EntityNotFoundError(HomeAssistantError): + """When a referenced entity was not found.""" diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index d3e3b4a3772..d62c0f4f192 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -11,6 +11,8 @@ Wetterwarnungen (Stufe 1) from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -93,29 +95,27 @@ class DwdWeatherWarningsSensor( entry_type=DeviceEntryType.SERVICE, ) - self.api = coordinator.api - @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the sensor.""" if self.entity_description.key == CURRENT_WARNING_SENSOR: - return self.api.current_warning_level + return self.coordinator.api.current_warning_level - return self.api.expected_warning_level + return self.coordinator.api.expected_warning_level @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" data = { - ATTR_REGION_NAME: self.api.warncell_name, - ATTR_REGION_ID: self.api.warncell_id, - ATTR_LAST_UPDATE: self.api.last_update, + ATTR_REGION_NAME: self.coordinator.api.warncell_name, + ATTR_REGION_ID: self.coordinator.api.warncell_id, + ATTR_LAST_UPDATE: self.coordinator.api.last_update, } if self.entity_description.key == CURRENT_WARNING_SENSOR: - searched_warnings = self.api.current_warnings + searched_warnings = self.coordinator.api.current_warnings else: - searched_warnings = self.api.expected_warnings + searched_warnings = self.coordinator.api.expected_warnings data[ATTR_WARNING_COUNT] = len(searched_warnings) @@ -142,4 +142,4 @@ class DwdWeatherWarningsSensor( @property def available(self) -> bool: """Could the device be accessed during the last update call.""" - return self.api.data_valid + return self.coordinator.api.data_valid diff --git a/homeassistant/components/dwd_weather_warnings/strings.json b/homeassistant/components/dwd_weather_warnings/strings.json index aa460dcc6d5..3f421d338a7 100644 --- a/homeassistant/components/dwd_weather_warnings/strings.json +++ b/homeassistant/components/dwd_weather_warnings/strings.json @@ -2,17 +2,22 @@ "config": { "step": { "user": { - "description": "To identify the desired region, the warncell ID / name is required.", + "description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'latitude' and 'longitude'.", "data": { - "region_identifier": "Warncell ID or name" + "region_identifier": "Warncell ID or name", + "region_device_tracker": "Device tracker entity" } } }, "error": { - "invalid_identifier": "The specified region identifier is invalid." + "no_identifier": "Either the region identifier or device tracker is required.", + "ambiguous_identifier": "The region identifier and device tracker can not be specified together.", + "invalid_identifier": "The specified region identifier / device tracker is invalid.", + "entity_not_found": "The specified device tracker entity was not found.", + "attribute_not_found": "The required `latitude` or `longitude` attribute was not found in the specified device tracker." }, "abort": { - "already_configured": "Warncell ID / name is already configured.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]" } }, diff --git a/homeassistant/components/dwd_weather_warnings/util.py b/homeassistant/components/dwd_weather_warnings/util.py new file mode 100644 index 00000000000..730ebf4b71e --- /dev/null +++ b/homeassistant/components/dwd_weather_warnings/util.py @@ -0,0 +1,39 @@ +"""Util functions for the dwd_weather_warnings integration.""" + +from __future__ import annotations + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .exceptions import EntityNotFoundError + + +def get_position_data( + hass: HomeAssistant, registry_id: str +) -> tuple[float, float] | None: + """Extract longitude and latitude from a device tracker.""" + registry = er.async_get(hass) + registry_entry = registry.async_get(registry_id) + if registry_entry is None: + raise EntityNotFoundError(f"Failed to find registry entry {registry_id}") + + entity = hass.states.get(registry_entry.entity_id) + if entity is None: + raise EntityNotFoundError(f"Failed to find entity {registry_entry.entity_id}") + + latitude = entity.attributes.get(ATTR_LATITUDE) + if not latitude: + raise AttributeError( + f"Failed to find attribute '{ATTR_LATITUDE}' in {registry_entry.entity_id}", + ATTR_LATITUDE, + ) + + longitude = entity.attributes.get(ATTR_LONGITUDE) + if not longitude: + raise AttributeError( + f"Failed to find attribute '{ATTR_LONGITUDE}' in {registry_entry.entity_id}", + ATTR_LONGITUDE, + ) + + return (latitude, longitude) diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index debfc335496..c9386999fae 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -67,21 +67,20 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: ebusdpy.init(server_address) - hass.data[DOMAIN] = EbusdData(server_address, circuit) - - sensor_config = { - CONF_MONITORED_CONDITIONS: monitored_conditions, - "client_name": name, - "sensor_types": SENSOR_TYPES[circuit], - } - load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config) - - hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) - - _LOGGER.debug("Ebusd integration setup completed") - return True except (TimeoutError, OSError): return False + hass.data[DOMAIN] = EbusdData(server_address, circuit) + sensor_config = { + CONF_MONITORED_CONDITIONS: monitored_conditions, + "client_name": name, + "sensor_types": SENSOR_TYPES[circuit], + } + load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config) + + hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) + + _LOGGER.debug("Ebusd integration setup completed") + return True class EbusdData: diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 8083d0efcb4..6f032fbaae9 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -73,6 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # The legacy Ecobee notify.notify service is deprecated + # was with HA Core 2024.5.0 and will be removed with HA core 2024.11.0 hass.async_create_task( discovery.async_load_platform( hass, @@ -97,7 +99,7 @@ class EcobeeData: ) -> None: """Initialize the Ecobee data object.""" self._hass = hass - self._entry = entry + self.entry = entry self.ecobee = Ecobee( config={ECOBEE_API_KEY: api_key, ECOBEE_REFRESH_TOKEN: refresh_token} ) @@ -117,7 +119,7 @@ class EcobeeData: _LOGGER.debug("Refreshing ecobee tokens and updating config entry") if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens): self._hass.config_entries.async_update_entry( - self._entry, + self.entry, data={ CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY], CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN], diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 0df8a42c566..11675c0bf61 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -12,7 +12,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + PRESET_AWAY, + PRESET_HOME, PRESET_NONE, + PRESET_SLEEP, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -60,9 +63,6 @@ PRESET_TEMPERATURE = "temp" PRESET_VACATION = "vacation" PRESET_HOLD_NEXT_TRANSITION = "next_transition" PRESET_HOLD_INDEFINITE = "indefinite" -AWAY_MODE = "awayMode" -PRESET_HOME = "home" -PRESET_SLEEP = "sleep" HAS_HEAT_PUMP = "hasHeatPump" DEFAULT_MIN_HUMIDITY = 15 @@ -103,6 +103,13 @@ ECOBEE_HVAC_ACTION_TO_HASS = { "compWaterHeater": None, } +ECOBEE_TO_HASS_PRESET = { + "Away": PRESET_AWAY, + "Home": PRESET_HOME, + "Sleep": PRESET_SLEEP, +} +HASS_TO_ECOBEE_PRESET = {v: k for k, v in ECOBEE_TO_HASS_PRESET.items()} + PRESET_TO_ECOBEE_HOLD = { PRESET_HOLD_NEXT_TRANSITION: "nextTransition", PRESET_HOLD_INDEFINITE: "indefinite", @@ -348,10 +355,6 @@ class Thermostat(ClimateEntity): self._attr_hvac_modes.insert(0, HVACMode.HEAT_COOL) self._attr_hvac_modes.append(HVACMode.OFF) - self._preset_modes = { - comfort["climateRef"]: comfort["name"] - for comfort in self.thermostat["program"]["climates"] - } self.update_without_throttle = False async def async_update(self) -> None: @@ -474,7 +477,7 @@ class Thermostat(ClimateEntity): return self.thermostat["runtime"]["desiredFanMode"] @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return current preset mode.""" events = self.thermostat["events"] for event in events: @@ -487,8 +490,8 @@ class Thermostat(ClimateEntity): ): return PRESET_AWAY_INDEFINITELY - if event["holdClimateRef"] in self._preset_modes: - return self._preset_modes[event["holdClimateRef"]] + if name := self.comfort_settings.get(event["holdClimateRef"]): + return ECOBEE_TO_HASS_PRESET.get(name, name) # Any hold not based on a climate is a temp hold return PRESET_TEMPERATURE @@ -499,7 +502,12 @@ class Thermostat(ClimateEntity): self.vacation = event["name"] return PRESET_VACATION - return self._preset_modes[self.thermostat["program"]["currentClimateRef"]] + if name := self.comfort_settings.get( + self.thermostat["program"]["currentClimateRef"] + ): + return ECOBEE_TO_HASS_PRESET.get(name, name) + + return None @property def hvac_mode(self): @@ -509,7 +517,10 @@ class Thermostat(ClimateEntity): @property def current_humidity(self) -> int | None: """Return the current humidity.""" - return self.thermostat["runtime"]["actualHumidity"] + try: + return int(self.thermostat["runtime"]["actualHumidity"]) + except KeyError: + return None @property def hvac_action(self): @@ -542,14 +553,14 @@ class Thermostat(ClimateEntity): return HVACAction.IDLE @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific state attributes.""" status = self.thermostat["equipmentStatus"] return { "fan": self.fan, - "climate_mode": self._preset_modes[ + "climate_mode": self.comfort_settings.get( self.thermostat["program"]["currentClimateRef"] - ], + ), "equipment_running": status, "fan_min_on_time": self.settings["fanMinOnTime"], } @@ -574,6 +585,8 @@ class Thermostat(ClimateEntity): def set_preset_mode(self, preset_mode: str) -> None: """Activate a preset.""" + preset_mode = HASS_TO_ECOBEE_PRESET.get(preset_mode, preset_mode) + if preset_mode == self.preset_mode: return @@ -602,25 +615,14 @@ class Thermostat(ClimateEntity): elif preset_mode == PRESET_NONE: self.data.ecobee.resume_program(self.thermostat_index) - elif preset_mode in self.preset_modes: - climate_ref = None - - for comfort in self.thermostat["program"]["climates"]: - if comfort["name"] == preset_mode: - climate_ref = comfort["climateRef"] + else: + for climate_ref, name in self.comfort_settings.items(): + if name == preset_mode: + preset_mode = climate_ref break - - if climate_ref is not None: - self.data.ecobee.set_climate_hold( - self.thermostat_index, - climate_ref, - self.hold_preference(), - self.hold_hours(), - ) else: _LOGGER.warning("Received unknown preset mode: %s", preset_mode) - else: self.data.ecobee.set_climate_hold( self.thermostat_index, preset_mode, @@ -629,11 +631,22 @@ class Thermostat(ClimateEntity): ) @property - def preset_modes(self): + def preset_modes(self) -> list[str] | None: """Return available preset modes.""" # 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] + return [ + ECOBEE_TO_HASS_PRESET.get(name, name) + for name in self.comfort_settings.values() + ] + [PRESET_AWAY_INDEFINITELY] + + @property + def comfort_settings(self) -> dict[str, str]: + """Return ecobee API comfort settings.""" + return { + comfort["climateRef"]: comfort["name"] + for comfort in self.thermostat["program"]["climates"] + } def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index e20acb5cfca..0eed0ab67f9 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -46,6 +46,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.HUMIDIFIER, + Platform.NOTIFY, Platform.NUMBER, Platform.SENSOR, Platform.WEATHER, diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 0de7de2e803..d9616383ab6 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -110,6 +110,14 @@ class EcobeeHumidifier(HumidifierEntity): """Return the desired humidity set point.""" return int(self.thermostat["runtime"]["desiredHumidity"]) + @property + def current_humidity(self) -> int | None: + """Return the current humidity.""" + try: + return int(self.thermostat["runtime"]["actualHumidity"]) + except KeyError: + return None + def set_mode(self, mode): """Set humidifier mode (auto, off, manual).""" if mode.lower() not in (self.available_modes): diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index f3f5b59a36f..7e461230600 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,6 +3,7 @@ "name": "ecobee", "codeowners": [], "config_flow": true, + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/ecobee", "homekit": { "models": ["EB", "ecobee*"] diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index b2f6ccb05c8..787130c403f 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -2,11 +2,23 @@ from __future__ import annotations -from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService +from functools import partial +from typing import Any + +from homeassistant.components.notify import ( + ATTR_TARGET, + BaseNotificationService, + NotifyEntity, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import Ecobee, EcobeeData from .const import DOMAIN +from .entity import EcobeeBaseEntity +from .repairs import migrate_notify_issue def get_service( @@ -18,18 +30,25 @@ def get_service( if discovery_info is None: return None - data = hass.data[DOMAIN] + data: EcobeeData = hass.data[DOMAIN] return EcobeeNotificationService(data.ecobee) class EcobeeNotificationService(BaseNotificationService): """Implement the notification service for the Ecobee thermostat.""" - def __init__(self, ecobee): + def __init__(self, ecobee: Ecobee) -> None: """Initialize the service.""" self.ecobee = ecobee - def send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message and raise issue.""" + migrate_notify_issue(self.hass) + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) + + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message.""" targets = kwargs.get(ATTR_TARGET) @@ -39,3 +58,33 @@ class EcobeeNotificationService(BaseNotificationService): for target in targets: thermostat_index = int(target) self.ecobee.send_message(thermostat_index, message) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ecobee thermostat.""" + data: EcobeeData = hass.data[DOMAIN] + async_add_entities( + EcobeeNotifyEntity(data, index) for index in range(len(data.ecobee.thermostats)) + ) + + +class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity): + """Implement the notification entity for the Ecobee thermostat.""" + + _attr_name = None + _attr_has_entity_name = True + + def __init__(self, data: EcobeeData, thermostat_index: int) -> None: + """Initialize the thermostat.""" + super().__init__(data, thermostat_index) + self._attr_unique_id = ( + f"{self.thermostat["identifier"]}_notify_{thermostat_index}" + ) + + def send_message(self, message: str) -> None: + """Send a message.""" + self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/ecobee/repairs.py b/homeassistant/components/ecobee/repairs.py new file mode 100644 index 00000000000..66474730b2f --- /dev/null +++ b/homeassistant/components/ecobee/repairs.py @@ -0,0 +1,37 @@ +"""Repairs support for Ecobee.""" + +from __future__ import annotations + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.repairs import RepairsFlow +from homeassistant.components.repairs.issue_handler import ConfirmRepairFlow +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir + +from .const import DOMAIN + + +@callback +def migrate_notify_issue(hass: HomeAssistant) -> None: + """Ensure an issue is registered.""" + ir.async_create_issue( + hass, + DOMAIN, + "migrate_notify", + breaks_in_ha_version="2024.11.0", + issue_domain=NOTIFY_DOMAIN, + is_fixable=True, + is_persistent=True, + translation_key="migrate_notify", + severity=ir.IssueSeverity.WARNING, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert issue_id == "migrate_notify" + return ConfirmRepairFlow() diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index b1d1df65417..1d64b6d6b94 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -163,5 +163,18 @@ } } } + }, + "issues": { + "migrate_notify": { + "title": "Migration of Ecobee notify service", + "fix_flow": { + "step": { + "confirm": { + "description": "The Ecobee `notify` service has been migrated. A new `notify` entity per Thermostat is available now.\n\nUpdate any automations to use the new `notify.send_message` exposed by these new entities. When this is done, fix this issue and restart Home Assistant.", + "title": "Disable legacy Ecobee notify service" + } + } + } + } } } diff --git a/homeassistant/components/ecoforest/coordinator.py b/homeassistant/components/ecoforest/coordinator.py index ae0b353a1df..3b04325bd50 100644 --- a/homeassistant/components/ecoforest/coordinator.py +++ b/homeassistant/components/ecoforest/coordinator.py @@ -32,7 +32,8 @@ class EcoforestCoordinator(DataUpdateCoordinator[Device]): """Fetch all device and sensor data from api.""" try: data = await self.api.get() - _LOGGER.debug("Ecoforest data: %s", data) - return data except EcoforestError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err + + _LOGGER.debug("Ecoforest data: %s", data) + return data diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index 8cf82f6237c..4a421113f5f 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -71,7 +71,7 @@ async def _validate_input( if errors: return errors - device_id = get_client_device_id() + device_id = get_client_device_id(hass, rest_url is not None) country = user_input[CONF_COUNTRY] rest_config = create_rest_config( aiohttp_client.async_get_clientsession(hass), @@ -303,7 +303,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): except AbortFlow as ex: if ex.reason == "already_configured": create_repair() - raise ex + raise if errors := result.get("errors"): error = errors["base"] diff --git a/homeassistant/components/ecovacs/const.py b/homeassistant/components/ecovacs/const.py index e5ef0760182..6b77404e935 100644 --- a/homeassistant/components/ecovacs/const.py +++ b/homeassistant/components/ecovacs/const.py @@ -12,8 +12,10 @@ CONF_OVERRIDE_MQTT_URL = "override_mqtt_url" CONF_VERIFY_MQTT_CERTIFICATE = "verify_mqtt_certificate" SUPPORTED_LIFESPANS = ( + LifeSpan.BLADE, LifeSpan.BRUSH, LifeSpan.FILTER, + LifeSpan.LENS_BRUSH, LifeSpan.SIDE_BRUSH, ) diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 5defcdf861f..6b6fe3128dd 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -43,7 +43,8 @@ class EcovacsController: self._hass = hass self._devices: list[Device] = [] self.legacy_devices: list[VacBot] = [] - self._device_id = get_client_device_id() + rest_url = config.get(CONF_OVERRIDE_REST_URL) + self._device_id = get_client_device_id(hass, rest_url is not None) country = config[CONF_COUNTRY] self._continent = get_continent(country) @@ -52,7 +53,7 @@ class EcovacsController: aiohttp_client.async_get_clientsession(self._hass), device_id=self._device_id, alpha_2_country=country, - override_rest_url=config.get(CONF_OVERRIDE_REST_URL), + override_rest_url=rest_url, ), config[CONF_USERNAME], md5(config[CONF_PASSWORD]), diff --git a/homeassistant/components/ecovacs/event.py b/homeassistant/components/ecovacs/event.py index daac4a626ae..fb4c25c7559 100644 --- a/homeassistant/components/ecovacs/event.py +++ b/homeassistant/components/ecovacs/event.py @@ -13,6 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .controller import EcovacsController from .entity import EcovacsEntity +from .util import get_name_key async def async_setup_entry( @@ -54,10 +55,7 @@ class EcovacsLastJobEventEntity( # we trigger only on job done return - event_type = event.status.name.lower() - if event.status == CleanJobStatus.MANUAL_STOPPED: - event_type = "manually_stopped" - + event_type = get_name_key(event.status) self._trigger_event(event_type) self.async_write_ha_state() diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index 2e2d897c455..44c577104dd 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -12,12 +12,18 @@ "relocate": { "default": "mdi:map-marker-question" }, + "reset_lifespan_blade": { + "default": "mdi:saw-blade" + }, "reset_lifespan_brush": { "default": "mdi:broom" }, "reset_lifespan_filter": { "default": "mdi:air-filter" }, + "reset_lifespan_lens_brush": { + "default": "mdi:broom" + }, "reset_lifespan_side_brush": { "default": "mdi:broom" } @@ -42,12 +48,18 @@ "error": { "default": "mdi:alert-circle" }, + "lifespan_blade": { + "default": "mdi:saw-blade" + }, "lifespan_brush": { "default": "mdi:broom" }, "lifespan_filter": { "default": "mdi:air-filter" }, + "lifespan_lens_brush": { + "default": "mdi:broom" + }, "lifespan_side_brush": { "default": "mdi:broom" }, diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 52753e6eb39..aad04d9ec87 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.9", "deebot-client==6.0.2"] + "requirements": ["py-sucks==0.9.9", "deebot-client==7.1.0"] } diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index 8a3def54e28..01d4c5aae6b 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -22,7 +22,7 @@ from .entity import ( EcovacsDescriptionEntity, EventT, ) -from .util import get_supported_entitites +from .util import get_name_key, get_supported_entitites @dataclass(kw_only=True, frozen=True) @@ -41,8 +41,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( EcovacsSelectEntityDescription[WaterInfoEvent]( device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.water, - current_option_fn=lambda e: e.amount.display_name, - options_fn=lambda water: [amount.display_name for amount in water.types], + current_option_fn=lambda e: get_name_key(e.amount), + options_fn=lambda water: [get_name_key(amount) for amount in water.types], key="water_amount", translation_key="water_amount", entity_category=EntityCategory.CONFIG, @@ -50,8 +50,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( EcovacsSelectEntityDescription[WorkModeEvent]( device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.clean.work_mode, - current_option_fn=lambda e: e.mode.display_name, - options_fn=lambda cap: [mode.display_name for mode in cap.types], + current_option_fn=lambda e: get_name_key(e.mode), + options_fn=lambda cap: [get_name_key(mode) for mode in cap.types], key="work_mode", translation_key="work_mode", entity_registry_enabled_default=False, diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index a21f57a7a24..bb27bd6941d 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -46,12 +46,18 @@ "relocate": { "name": "Relocate" }, + "reset_lifespan_blade": { + "name": "Reset blade lifespan" + }, "reset_lifespan_brush": { "name": "Reset main brush lifespan" }, "reset_lifespan_filter": { "name": "Reset filter lifespan" }, + "reset_lifespan_lens_brush": { + "name": "Reset lens brush lifespan" + }, "reset_lifespan_side_brush": { "name": "Reset side brushes lifespan" } @@ -92,12 +98,18 @@ } } }, + "lifespan_blade": { + "name": "Blade lifespan" + }, "lifespan_brush": { "name": "Main brush lifespan" }, "lifespan_filter": { "name": "Filter lifespan" }, + "lifespan_lens_brush": { + "name": "Lens brush lifespan" + }, "lifespan_side_brush": { "name": "Side brushes lifespan" }, @@ -227,7 +239,7 @@ }, "deprecated_yaml_import_issue_continent_not_match": { "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent '{continent}' is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent '{continent}' is not applicable, please open an issue on [GitHub]({github_issue_url})." + "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent \"{continent}\" is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent \"{continent}\" is not applicable, please open an issue on [GitHub]({github_issue_url})." } }, "selector": { diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index 14e69cd4b61..9d692bbbb8f 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -2,12 +2,16 @@ from __future__ import annotations +from enum import Enum import random import string from typing import TYPE_CHECKING from deebot_client.capabilities import Capabilities +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, @@ -18,8 +22,11 @@ if TYPE_CHECKING: from .controller import EcovacsController -def get_client_device_id() -> str: +def get_client_device_id(hass: HomeAssistant, self_hosted: bool) -> str: """Get client device id.""" + if self_hosted: + return f"HA-{slugify(hass.config.location_name)}" + return "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(8) ) @@ -38,3 +45,9 @@ def get_supported_entitites( if isinstance(device.capabilities, description.device_capabilities) if (capability := description.capability_fn(device.capabilities)) ] + + +@callback +def get_name_key(enum: Enum) -> str: + """Return the lower case name of the enum.""" + return enum.name.lower() diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index d5016ab683d..0e990645d7c 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -33,6 +33,7 @@ from homeassistant.util import slugify from .const import DOMAIN from .controller import EcovacsController from .entity import EcovacsEntity +from .util import get_name_key _LOGGER = logging.getLogger(__name__) @@ -242,7 +243,7 @@ class EcovacsVacuum( self._rooms: list[Room] = [] self._attr_fan_speed_list = [ - level.display_name for level in capabilities.fan_speed.types + get_name_key(level) for level in capabilities.fan_speed.types ] async def async_added_to_hass(self) -> None: @@ -254,7 +255,7 @@ class EcovacsVacuum( self.async_write_ha_state() async def on_fan_speed(event: FanSpeedEvent) -> None: - self._attr_fan_speed = event.speed.display_name + self._attr_fan_speed = get_name_key(event.speed) self.async_write_ha_state() async def on_rooms(event: RoomsEvent) -> None: diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py index e4aecc1c07b..db7d2e0989d 100644 --- a/homeassistant/components/ecowitt/diagnostics.py +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -22,7 +22,7 @@ async def async_get_device_diagnostics( station = ecowitt.stations[station_id] - data = { + return { "device": { "name": station.station, "model": station.model, @@ -36,5 +36,3 @@ async def async_get_device_diagnostics( if sensor.station.key == station_id }, } - - return data diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index c58396ae947..dec4750d219 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -102,7 +102,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): def lookupstatusfromcode(self, statuscode): """Look at the rs_codes and returns the status from the code.""" - status = next( + return next( ( status_group.upper() for status_group, codes in self._rs_codes.items() @@ -111,7 +111,6 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): ), "UNKNOWN", ) - return status def parsestatus(self, status): """Parse the status.""" diff --git a/homeassistant/components/electric_kiwi/oauth2.py b/homeassistant/components/electric_kiwi/oauth2.py index 864550991f5..9a6c4cd22a5 100644 --- a/homeassistant/components/electric_kiwi/oauth2.py +++ b/homeassistant/components/electric_kiwi/oauth2.py @@ -73,5 +73,4 @@ class ElectricKiwiLocalOAuth2Implementation(AuthImplementation): resp = await session.post(self.token_url, data=data, headers=headers) resp.raise_for_status() - resp_json = cast(dict, await resp.json()) - return resp_json + return cast(dict, await resp.json()) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 5991c502ef6..9a71c86478b 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -174,9 +174,7 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN): or hostname_from_url(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) - ) + 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(): diff --git a/homeassistant/components/elvia/config_flow.py b/homeassistant/components/elvia/config_flow.py index 4cf311e780e..2db6e4bb2b5 100644 --- a/homeassistant/components/elvia/config_flow.py +++ b/homeassistant/components/elvia/config_flow.py @@ -8,14 +8,14 @@ from typing import TYPE_CHECKING, Any from elvia import Elvia, error as ElviaError 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.util import dt as dt_util from .const import CONF_METERING_POINT_ID, DOMAIN, LOGGER -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ElviaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Elvia.""" def __init__(self) -> None: @@ -26,7 +26,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -75,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select_meter( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle selecting a metering point ID.""" if TYPE_CHECKING: assert self._metering_point_ids is not None @@ -103,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, api_token: str, metering_point_id: str, - ) -> config_entries.ConfigFlowResult: + ) -> 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/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index b4208c1f3f6..91876d81508 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -16,10 +16,16 @@ from homeassistant.components import ( script, ) from homeassistant.const import CONF_ENTITIES, CONF_TYPE -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + Event, + EventStateChangedData, + 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, ) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 91c4440d875..8194d31823d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -65,11 +65,8 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, State -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.core import Event, EventStateChangedData, State +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.json import json_loads from homeassistant.util.network import is_local diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index ff3591e0066..14baa5b5d04 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -6,6 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "iot_class": "local_push", - "quality_scale": "internal", - "requirements": ["aiohttp_cors==0.7.0"] + "quality_scale": "internal" } diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 739f3b04ec0..214658b7c0e 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "iot_class": "local_push", "loggers": ["emulated_roku"], - "requirements": ["emulated-roku==0.2.1"] + "requirements": ["emulated-roku==0.3.0"] } diff --git a/homeassistant/components/energenie_power_sockets/__init__.py b/homeassistant/components/energenie_power_sockets/__init__.py new file mode 100644 index 00000000000..12ddb0d1389 --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/__init__.py @@ -0,0 +1,44 @@ +"""Energenie Power-Sockets (EGPS) integration.""" + +from pyegps import PowerStripUSB, get_device +from pyegps.exceptions import MissingLibrary, UsbError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady + +from .const import CONF_DEVICE_API_ID, DOMAIN + +PLATFORMS = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Energenie Power Sockets.""" + try: + powerstrip: PowerStripUSB | None = get_device(entry.data[CONF_DEVICE_API_ID]) + + except (MissingLibrary, UsbError) as ex: + raise ConfigEntryError("Can't access usb devices.") from ex + + if powerstrip is None: + raise ConfigEntryNotReady( + "Can't access Energenie Power Sockets, will retry later." + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = powerstrip + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + powerstrip = hass.data[DOMAIN].pop(entry.entry_id) + powerstrip.release() + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok diff --git a/homeassistant/components/energenie_power_sockets/config_flow.py b/homeassistant/components/energenie_power_sockets/config_flow.py new file mode 100644 index 00000000000..ab39427f15a --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/config_flow.py @@ -0,0 +1,55 @@ +"""ConfigFlow for Energenie-Power-Sockets devices.""" + +from typing import Any + +from pyegps import get_device, search_for_devices +from pyegps.exceptions import MissingLibrary, UsbError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult + +from .const import CONF_DEVICE_API_ID, DOMAIN, LOGGER + + +class EGPSConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle the config flow for EGPS devices.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Initiate user flow.""" + + if user_input is not None: + dev_id = user_input[CONF_DEVICE_API_ID] + dev = await self.hass.async_add_executor_job(get_device, dev_id) + if dev is not None: + await self.async_set_unique_id(dev.device_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=dev_id, + data={CONF_DEVICE_API_ID: dev_id}, + ) + return self.async_abort(reason="device_not_found") + + currently_configured = self._async_current_ids(include_ignore=True) + try: + found_devices = await self.hass.async_add_executor_job(search_for_devices) + except (MissingLibrary, UsbError): + LOGGER.exception("Unable to access USB devices") + return self.async_abort(reason="usb_error") + + devices = [ + d + for d in found_devices + if d.get_device_type() == "PowerStrip" + and d.device_id not in currently_configured + ] + LOGGER.debug("Found %d devices", len(devices)) + if len(devices) > 0: + options = {d.device_id: f"{d.name} ({d.device_id})" for d in devices} + data_schema = {CONF_DEVICE_API_ID: vol.In(options)} + else: + return self.async_abort(reason="no_device") + + return self.async_show_form(step_id="user", data_schema=vol.Schema(data_schema)) diff --git a/homeassistant/components/energenie_power_sockets/const.py b/homeassistant/components/energenie_power_sockets/const.py new file mode 100644 index 00000000000..a02373815c2 --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/const.py @@ -0,0 +1,8 @@ +"""Constants for Energenie Power Sockets.""" + +import logging + +LOGGER = logging.getLogger(__package__) + +CONF_DEVICE_API_ID = "api-device-id" +DOMAIN = "energenie_power_sockets" diff --git a/homeassistant/components/energenie_power_sockets/manifest.json b/homeassistant/components/energenie_power_sockets/manifest.json new file mode 100644 index 00000000000..8a55a539e7f --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "energenie_power_sockets", + "name": "Energenie Power Sockets", + "codeowners": ["@gnumpi"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/energenie_power_sockets", + "integration_type": "device", + "iot_class": "local_polling", + "loggers": ["pyegps"], + "requirements": ["pyegps==0.2.5"] +} diff --git a/homeassistant/components/energenie_power_sockets/strings.json b/homeassistant/components/energenie_power_sockets/strings.json new file mode 100644 index 00000000000..e193b06b25f --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/strings.json @@ -0,0 +1,27 @@ +{ + "title": "Energenie Power Sockets Integration.", + "config": { + "step": { + "user": { + "title": "Searching for Energenie-Power-Sockets Devices.", + "description": "Choose a discovered device.", + "data": { + "device": "[%key:common::config_flow::data::device%]" + } + } + }, + "abort": { + "usb_error": "Couldn't access USB devices!", + "no_device": "Unable to discover any (new) supported device.", + "device_not_found": "No device was found for the given id.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "switch": { + "socket": { + "name": "Socket {socket_id}" + } + } + } +} diff --git a/homeassistant/components/energenie_power_sockets/switch.py b/homeassistant/components/energenie_power_sockets/switch.py new file mode 100644 index 00000000000..1d5b9ed5197 --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/switch.py @@ -0,0 +1,77 @@ +"""Switch implementation for Energenie-Power-Sockets Platform.""" + +from typing import Any + +from pyegps import __version__ as PYEGPS_VERSION +from pyegps.exceptions import EgpsException +from pyegps.powerstrip import PowerStrip + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add EGPS sockets for passed config_entry in HA.""" + powerstrip: PowerStrip = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + ( + EGPowerStripSocket(powerstrip, socket) + for socket in range(powerstrip.numberOfSockets) + ), + update_before_add=True, + ) + + +class EGPowerStripSocket(SwitchEntity): + """Represents a socket of an Energenie-Socket-Strip.""" + + _attr_device_class = SwitchDeviceClass.OUTLET + _attr_has_entity_name = True + _attr_translation_key = "socket" + + def __init__(self, dev: PowerStrip, socket: int) -> None: + """Initiate a new socket.""" + self._dev = dev + self._socket = socket + self._attr_translation_placeholders = {"socket_id": str(socket)} + + self._attr_unique_id = f"{dev.device_id}_{socket}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, dev.device_id)}, + name=dev.name, + manufacturer=dev.manufacturer, + model=dev.name, + sw_version=PYEGPS_VERSION, + ) + + def turn_on(self, **kwargs: Any) -> None: + """Switch the socket on.""" + try: + self._dev.switch_on(self._socket) + except EgpsException as err: + raise HomeAssistantError(f"Couldn't access USB device: {err}") from err + + def turn_off(self, **kwargs: Any) -> None: + """Switch the socket off.""" + try: + self._dev.switch_off(self._socket) + except EgpsException as err: + raise HomeAssistantError(f"Couldn't access USB device: {err}") from err + + def update(self) -> None: + """Read the current state from the device.""" + try: + self._attr_is_on = self._dev.get_status(self._socket) + except EgpsException as err: + raise HomeAssistantError(f"Couldn't access USB device: {err}") from err diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d4533b2fcc8..d0da07da37c 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -136,6 +136,9 @@ class DeviceConsumption(TypedDict): # This is an ever increasing value stat_consumption: str + # An optional custom name for display in energy graphs + name: str | None + class EnergyPreferences(TypedDict): """Dictionary holding the energy data.""" @@ -287,6 +290,7 @@ ENERGY_SOURCE_SCHEMA = vol.All( DEVICE_CONSUMPTION_SCHEMA = vol.Schema( { vol.Required("stat_consumption"): str, + vol.Optional("name"): str, } ) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 37930e31af0..147d8f3e26a 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping import copy from dataclasses import dataclass import logging @@ -167,8 +167,7 @@ class SensorManager: if adapter.flow_type is None: self._process_sensor_data( adapter, - # Opting out of the type complexity because can't get it to work - energy_source, # type: ignore[arg-type] + energy_source, to_add, to_remove, ) @@ -177,8 +176,7 @@ class SensorManager: for flow in energy_source[adapter.flow_type]: # type: ignore[typeddict-item] self._process_sensor_data( adapter, - # Opting out of the type complexity because can't get it to work - flow, # type: ignore[arg-type] + flow, to_add, to_remove, ) @@ -189,7 +187,7 @@ class SensorManager: def _process_sensor_data( self, adapter: SourceAdapter, - config: dict, + config: Mapping[str, Any], to_add: list[EnergyCostSensor], to_remove: dict[tuple[str, str | None, str], EnergyCostSensor], ) -> None: @@ -241,7 +239,7 @@ class EnergyCostSensor(SensorEntity): def __init__( self, adapter: SourceAdapter, - config: dict, + config: Mapping[str, Any], ) -> None: """Initialize the sensor.""" super().__init__() @@ -456,7 +454,7 @@ class EnergyCostSensor(SensorEntity): await super().async_will_remove_from_hass() @callback - def update_config(self, config: dict) -> None: + def update_config(self, config: Mapping[str, Any]) -> None: """Update the config.""" self._config = config diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index 2dd45a8be4d..2b5b71d3e2f 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -31,7 +31,7 @@ from .data import ( EnergyPreferencesUpdate, async_get_manager, ) -from .types import EnergyPlatform, GetSolarForecastType +from .types import EnergyPlatform, GetSolarForecastType, SolarForecastType from .validate import async_validate EnergyWebSocketCommandHandler = Callable[ @@ -203,19 +203,18 @@ async def ws_solar_forecast( for source in manager.data["energy_sources"]: if ( source["type"] != "solar" - or source.get("config_entry_solar_forecast") is None + or (solar_forecast := source.get("config_entry_solar_forecast")) is None ): continue - # typing is not catching the above guard for config_entry_solar_forecast being none - for config_entry in source["config_entry_solar_forecast"]: # type: ignore[union-attr] - config_entries[config_entry] = None + for entry in solar_forecast: + config_entries[entry] = None if not config_entries: connection.send_result(msg["id"], {}) return - forecasts = {} + forecasts: dict[str, SolarForecastType] = {} forecast_platforms = await async_get_energy_platforms(hass) diff --git a/homeassistant/components/enigma2/__init__.py b/homeassistant/components/enigma2/__init__.py index 11cd4d9a804..241ca7444fb 100644 --- a/homeassistant/components/enigma2/__init__.py +++ b/homeassistant/components/enigma2/__init__.py @@ -1 +1,48 @@ """Support for Enigma2 devices.""" + +from openwebif.api import OpenWebIfDevice +from yarl import URL + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +from .const import DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Enigma2 from a config entry.""" + base_url = URL.build( + scheme="http" if not entry.data[CONF_SSL] else "https", + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + user=entry.data.get(CONF_USERNAME), + password=entry.data.get(CONF_PASSWORD), + ) + + session = async_create_clientsession( + hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = OpenWebIfDevice(session) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if 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/enigma2/config_flow.py b/homeassistant/components/enigma2/config_flow.py new file mode 100644 index 00000000000..ac57bd9d0fa --- /dev/null +++ b/homeassistant/components/enigma2/config_flow.py @@ -0,0 +1,165 @@ +"""Config flow for Enigma2.""" + +from typing import Any + +from aiohttp.client_exceptions import ClientError +from openwebif.api import OpenWebIfDevice +from openwebif.error import InvalidAuthError +import voluptuous as vol +from yarl import URL + +from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.helpers import selector +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue + +from .const import ( + CONF_DEEP_STANDBY, + CONF_SOURCE_BOUQUET, + CONF_USE_CHANNEL_ICON, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN, +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): selector.TextSelector(), + vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=65535, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Coerce(int), + ), + vol.Optional(CONF_USERNAME): selector.TextSelector(), + vol.Optional(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + vol.Required(CONF_SSL, default=DEFAULT_SSL): selector.BooleanSelector(), + vol.Required( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL + ): selector.BooleanSelector(), + } +) + + +class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Enigma2.""" + + DATA_KEYS = ( + CONF_HOST, + CONF_PORT, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SSL, + CONF_VERIFY_SSL, + ) + OPTIONS_KEYS = (CONF_DEEP_STANDBY, CONF_SOURCE_BOUQUET, CONF_USE_CHANNEL_ICON) + + async def validate_user_input( + self, user_input: dict[str, Any] + ) -> dict[str, str] | None: + """Validate user input.""" + + errors = None + + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + base_url = URL.build( + scheme="http" if not user_input[CONF_SSL] else "https", + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + user=user_input.get(CONF_USERNAME), + password=user_input.get(CONF_PASSWORD), + ) + + session = async_create_clientsession( + self.hass, verify_ssl=user_input[CONF_VERIFY_SSL], base_url=base_url + ) + + try: + about = await OpenWebIfDevice(session).get_about() + except InvalidAuthError: + errors = {"base": "invalid_auth"} + except ClientError: + errors = {"base": "cannot_connect"} + except Exception: # pylint: disable=broad-except + errors = {"base": "unknown"} + else: + await self.async_set_unique_id(about["info"]["ifaces"][0]["mac"]) + self._abort_if_unique_id_configured() + + return errors + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step.""" + if user_input is None: + return self.async_show_form(step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA) + + if errors := await self.validate_user_input(user_input): + return self.async_show_form( + step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA, errors=errors + ) + return self.async_create_entry(data=user_input, title=user_input[CONF_HOST]) + + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: + """Handle the import step.""" + if CONF_PORT not in user_input: + user_input[CONF_PORT] = DEFAULT_PORT + if CONF_SSL not in user_input: + user_input[CONF_SSL] = DEFAULT_SSL + user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL + + data = {key: user_input[key] for key in user_input if key in self.DATA_KEYS} + options = { + key: user_input[key] for key in user_input if key in self.OPTIONS_KEYS + } + + if errors := await self.validate_user_input(user_input): + async_create_issue( + self.hass, + DOMAIN, + f"deprecated_yaml_{DOMAIN}_import_issue_{errors["base"]}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{errors["base"]}", + translation_placeholders={ + "url": "/config/integrations/dashboard/add?domain=enigma2" + }, + ) + return self.async_abort(reason=errors["base"]) + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Enigma2", + }, + ) + return self.async_create_entry( + data=data, title=data[CONF_HOST], options=options + ) diff --git a/homeassistant/components/enigma2/const.py b/homeassistant/components/enigma2/const.py index 277efad50eb..d7508fee64e 100644 --- a/homeassistant/components/enigma2/const.py +++ b/homeassistant/components/enigma2/const.py @@ -16,3 +16,4 @@ DEFAULT_PASSWORD = "dreambox" DEFAULT_DEEP_STANDBY = False DEFAULT_SOURCE_BOUQUET = "" DEFAULT_MAC_ADDRESS = "" +DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 0de4adc13b8..ef08314e541 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -2,7 +2,9 @@ "domain": "enigma2", "name": "Enigma2 (OpenWebif)", "codeowners": ["@autinerd"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/enigma2", + "integration_type": "device", "iot_class": "local_polling", "loggers": ["openwebif"], "requirements": ["openwebifpy==4.2.4"] diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index afe8a426c72..037d82cd6c0 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -9,7 +9,6 @@ from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedEr from openwebif.api import OpenWebIfDevice from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption import voluptuous as vol -from yarl import URL from homeassistant.components.media_player import ( MediaPlayerEntity, @@ -17,6 +16,7 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -26,10 +26,9 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -47,6 +46,7 @@ from .const import ( DEFAULT_SSL, DEFAULT_USE_CHANNEL_ICON, DEFAULT_USERNAME, + DOMAIN, ) ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" @@ -81,49 +81,44 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up of an enigma2 media player.""" - if discovery_info: - # Discovery gives us the streaming service port (8001) - # which is not useful as OpenWebif never runs on that port. - # So use the default port instead. - config[CONF_PORT] = DEFAULT_PORT - config[CONF_NAME] = discovery_info["hostname"] - config[CONF_HOST] = discovery_info["host"] - config[CONF_USERNAME] = DEFAULT_USERNAME - config[CONF_PASSWORD] = DEFAULT_PASSWORD - config[CONF_SSL] = DEFAULT_SSL - config[CONF_USE_CHANNEL_ICON] = DEFAULT_USE_CHANNEL_ICON - config[CONF_MAC_ADDRESS] = DEFAULT_MAC_ADDRESS - config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY - config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET - base_url = URL.build( - scheme="https" if config[CONF_SSL] else "http", - host=config[CONF_HOST], - port=config.get(CONF_PORT), - user=config.get(CONF_USERNAME), - password=config.get(CONF_PASSWORD), + entry_data = { + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_SSL: config[CONF_SSL], + CONF_USE_CHANNEL_ICON: config[CONF_USE_CHANNEL_ICON], + CONF_DEEP_STANDBY: config[CONF_DEEP_STANDBY], + CONF_SOURCE_BOUQUET: config[CONF_SOURCE_BOUQUET], + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_data + ) ) - session = async_create_clientsession(hass, verify_ssl=False, base_url=base_url) - device = OpenWebIfDevice( - host=session, - turn_off_to_deep=config.get(CONF_DEEP_STANDBY, False), - source_bouquet=config.get(CONF_SOURCE_BOUQUET), - ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Enigma2 media player platform.""" - try: - about = await device.get_about() - except ClientConnectorError as err: - raise PlatformNotReady from err - - async_add_entities([Enigma2Device(config[CONF_NAME], device, about)]) + device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id] + about = await device.get_about() + device.mac_address = about["info"]["ifaces"][0]["mac"] + entity = Enigma2Device(entry, device, about) + async_add_entities([entity]) class Enigma2Device(MediaPlayerEntity): """Representation of an Enigma2 box.""" _attr_has_entity_name = True + _attr_name = None _attr_media_content_type = MediaType.TVSHOW _attr_supported_features = ( @@ -139,14 +134,23 @@ class Enigma2Device(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, name: str, device: OpenWebIfDevice, about: dict) -> None: + def __init__( + self, entry: ConfigEntry, device: OpenWebIfDevice, about: dict + ) -> None: """Initialize the Enigma2 device.""" self._device: OpenWebIfDevice = device - self._device.mac_address = about["info"]["ifaces"][0]["mac"] + self._entry = entry - self._attr_name = name self._attr_unique_id = device.mac_address + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.mac_address)}, + manufacturer=about["info"]["brand"], + model=about["info"]["model"], + configuration_url=device.base, + name=entry.data[CONF_HOST], + ) + async def async_turn_off(self) -> None: """Turn off media player.""" if self._device.turn_off_to_deep: diff --git a/homeassistant/components/enigma2/strings.json b/homeassistant/components/enigma2/strings.json new file mode 100644 index 00000000000..ddeb59ea6d5 --- /dev/null +++ b/homeassistant/components/enigma2/strings.json @@ -0,0 +1,43 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Please enter the connection details of your device.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_unknown": { + "title": "The Enigma2 YAML configuration import failed", + "description": "Configuring Enigma2 using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure connection to the device works, the authentication details are correct and restart Home Assistant to try again or remove the Enigma2 YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The Enigma2 YAML configuration import failed", + "description": "Configuring Enigma2 using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure the authentication details are correct and restart Home Assistant to try again or remove the Enigma2 YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The Enigma2 YAML configuration import failed", + "description": "Configuring Enigma2 using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure connection to the device works and restart Home Assistant to try again or remove the Enigma2 YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } + } +} diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py index 1137eb23256..157d58bbf23 100644 --- a/homeassistant/components/enocean/config_flow.py +++ b/homeassistant/components/enocean/config_flow.py @@ -81,10 +81,7 @@ class EnOceanFlowHandler(ConfigFlow, domain=DOMAIN): async def validate_enocean_conf(self, user_input) -> bool: """Return True if the user_input contains a valid dongle path.""" dongle_path = user_input[CONF_DEVICE] - path_is_valid = await self.hass.async_add_executor_job( - dongle.validate_path, dongle_path - ) - return path_is_valid + return await self.hass.async_add_executor_job(dongle.validate_path, dongle_path) def create_enocean_entry(self, user_input): """Create an entry for the provided configuration.""" diff --git a/homeassistant/components/enocean/dongle.py b/homeassistant/components/enocean/dongle.py index 6402b4c3a28..2d9a3f8787e 100644 --- a/homeassistant/components/enocean/dongle.py +++ b/homeassistant/components/enocean/dongle.py @@ -82,7 +82,7 @@ def validate_path(path: str): # Creating the serial communicator will raise an exception # if it cannot connect SerialCommunicator(port=path) - return True except serial.SerialException as exception: _LOGGER.warning("Dongle path %s is invalid: %s", path, str(exception)) return False + return True diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 2407f807eb7..322f909437a 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -46,6 +46,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.""" + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator.async_cancel_token_refresh() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index dfa619f07d8..dbd8498467f 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from operator import attrgetter from pyenphase import EnvoyEncharge, EnvoyEnpower @@ -36,7 +37,7 @@ ENCHARGE_SENSORS = ( translation_key="communicating", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda encharge: encharge.communicating, + value_fn=attrgetter("communicating"), ), EnvoyEnchargeBinarySensorEntityDescription( key="dc_switch", @@ -60,7 +61,7 @@ ENPOWER_SENSORS = ( translation_key="communicating", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda enpower: enpower.communicating, + value_fn=attrgetter("communicating"), ), EnvoyEnpowerBinarySensorEntityDescription( key="mains_oper_state", diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 13894d423d6..5f859d16142 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -89,6 +89,14 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" + if _LOGGER.isEnabledFor(logging.DEBUG): + current_hosts = self._async_current_hosts() + _LOGGER.debug( + "Zeroconf ip %s processing %s, current hosts: %s", + discovery_info.ip_address.version, + discovery_info.host, + current_hosts, + ) if discovery_info.ip_address.version != 4: return self.async_abort(reason="not_ipv4_address") serial = discovery_info.properties["serialnum"] @@ -96,17 +104,27 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) + _LOGGER.debug( + "Zeroconf ip %s, fw %s, no existing entry with serial %s", + self.ip_address, + self.protovers, + serial, + ) for entry in self._async_current_entries(include_ignore=False): if ( entry.unique_id is None and CONF_HOST in entry.data and entry.data[CONF_HOST] == self.ip_address ): + _LOGGER.debug( + "Zeroconf update envoy with this ip and blank serial in unique_id", + ) title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY return self.async_update_reload_and_abort( entry, title=title, unique_id=serial, reason="already_configured" ) + _LOGGER.debug("Zeroconf ip %s to step user", self.ip_address) return await self.async_step_user() async def async_step_reauth( diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index c8152d44726..04f93098ad9 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -83,9 +83,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): def _async_mark_setup_complete(self) -> None: """Mark setup as complete and setup token refresh if needed.""" self._setup_complete = True - if self._cancel_token_refresh: - self._cancel_token_refresh() - self._cancel_token_refresh = None + self.async_cancel_token_refresh() if not isinstance(self.envoy.auth, EnvoyTokenAuth): return self._cancel_token_refresh = async_track_time_interval( @@ -147,8 +145,6 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._async_mark_setup_complete() # dump all received data in debug mode to assist troubleshooting envoy_data = await envoy.update() - _LOGGER.debug("Envoy data: %s", envoy_data) - return envoy_data.raw except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: # token likely expired or firmware changed, try to re-authenticate @@ -157,5 +153,14 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from err except EnvoyError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err + _LOGGER.debug("Envoy data: %s", envoy_data) + return envoy_data.raw raise RuntimeError("Unreachable code in _async_update_data") # pragma: no cover + + @callback + def async_cancel_token_refresh(self) -> None: + """Cancel token refresh.""" + if self._cancel_token_refresh: + self._cancel_token_refresh() + self._cancel_token_refresh = None diff --git a/homeassistant/components/enphase_envoy/number.py b/homeassistant/components/enphase_envoy/number.py index 61d9aabb469..38bb18ad768 100644 --- a/homeassistant/components/enphase_envoy/number.py +++ b/homeassistant/components/enphase_envoy/number.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass +from operator import attrgetter from typing import Any from pyenphase import Envoy, EnvoyDryContactSettings @@ -47,14 +48,14 @@ RELAY_ENTITIES = ( translation_key="cutoff_battery_level", device_class=NumberDeviceClass.BATTERY, entity_category=EntityCategory.CONFIG, - value_fn=lambda relay: relay.soc_low, + value_fn=attrgetter("soc_low"), ), EnvoyRelayNumberEntityDescription( key="soc_high", translation_key="restore_battery_level", device_class=NumberDeviceClass.BATTERY, entity_category=EntityCategory.CONFIG, - value_fn=lambda relay: relay.soc_high, + value_fn=attrgetter("soc_high"), ), ) @@ -63,7 +64,7 @@ STORAGE_RESERVE_SOC_ENTITY = EnvoyStorageSettingsNumberEntityDescription( translation_key="reserve_soc", native_unit_of_measurement=PERCENTAGE, device_class=NumberDeviceClass.BATTERY, - value_fn=lambda storage_settings: storage_settings.reserved_soc, + value_fn=attrgetter("reserved_soc"), update_fn=lambda envoy, value: envoy.set_reserve_soc(int(value)), ) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 329dc67e9e1..13445d8897a 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -6,6 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass, replace import datetime import logging +from operator import attrgetter from typing import TYPE_CHECKING from pyenphase import ( @@ -73,7 +74,7 @@ INVERTER_SENSORS = ( native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, - value_fn=lambda inverter: inverter.last_report_watts, + value_fn=attrgetter("last_report_watts"), ), EnvoyInverterSensorEntityDescription( key=LAST_REPORTED_KEY, @@ -102,7 +103,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda production: production.watts_now, + value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyProductionSensorEntityDescription( @@ -113,7 +114,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, - value_fn=lambda production: production.watt_hours_today, + value_fn=attrgetter("watt_hours_today"), on_phase=None, ), EnvoyProductionSensorEntityDescription( @@ -123,7 +124,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, - value_fn=lambda production: production.watt_hours_last_7_days, + value_fn=attrgetter("watt_hours_last_7_days"), on_phase=None, ), EnvoyProductionSensorEntityDescription( @@ -134,7 +135,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda production: production.watt_hours_lifetime, + value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) @@ -146,6 +147,7 @@ PRODUCTION_PHASE_SENSORS = { 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}"}, ) @@ -172,7 +174,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda consumption: consumption.watts_now, + value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( @@ -183,7 +185,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, - value_fn=lambda consumption: consumption.watt_hours_today, + value_fn=attrgetter("watt_hours_today"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( @@ -193,7 +195,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, - value_fn=lambda consumption: consumption.watt_hours_last_7_days, + value_fn=attrgetter("watt_hours_last_7_days"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( @@ -204,7 +206,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda consumption: consumption.watt_hours_lifetime, + value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) @@ -216,6 +218,7 @@ CONSUMPTION_PHASE_SENSORS = { 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}"}, ) @@ -245,7 +248,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_delivered, + value_fn=attrgetter("energy_delivered"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -256,7 +259,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_received, + value_fn=attrgetter("energy_received"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -267,7 +270,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda ct: ct.active_power, + value_fn=attrgetter("active_power"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -276,10 +279,9 @@ CT_NET_CONSUMPTION_SENSORS = ( native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, - suggested_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=1, entity_registry_enabled_default=False, - value_fn=lambda ct: ct.frequency, + value_fn=attrgetter("frequency"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -291,7 +293,7 @@ CT_NET_CONSUMPTION_SENSORS = ( suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, - value_fn=lambda ct: ct.voltage, + value_fn=attrgetter("voltage"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -300,7 +302,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_registry_enabled_default=False, - value_fn=lambda ct: ct.metering_status, + value_fn=attrgetter("metering_status"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -336,7 +338,7 @@ CT_PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_registry_enabled_default=False, - value_fn=lambda ct: ct.metering_status, + value_fn=attrgetter("metering_status"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -373,7 +375,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_delivered, + value_fn=attrgetter("energy_delivered"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -384,7 +386,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_received, + value_fn=attrgetter("energy_received"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -395,7 +397,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda ct: ct.active_power, + value_fn=attrgetter("active_power"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -407,7 +409,7 @@ CT_STORAGE_SENSORS = ( suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, - value_fn=lambda ct: ct.voltage, + value_fn=attrgetter("voltage"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -416,7 +418,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_registry_enabled_default=False, - value_fn=lambda ct: ct.metering_status, + value_fn=attrgetter("metering_status"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -470,7 +472,7 @@ ENCHARGE_INVENTORY_SENSORS = ( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda encharge: encharge.temperature, + value_fn=attrgetter("temperature"), ), EnvoyEnchargeSensorEntityDescription( key=LAST_REPORTED_KEY, @@ -485,7 +487,7 @@ ENCHARGE_POWER_SENSORS = ( key="soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - value_fn=lambda encharge: encharge.soc, + value_fn=attrgetter("soc"), ), EnvoyEnchargePowerSensorEntityDescription( key="apparent_power_mva", @@ -514,7 +516,7 @@ ENPOWER_SENSORS = ( key="temperature", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda enpower: enpower.temperature, + value_fn=attrgetter("temperature"), ), EnvoyEnpowerSensorEntityDescription( key=LAST_REPORTED_KEY, @@ -542,35 +544,35 @@ ENCHARGE_AGGREGATE_SENSORS = ( key="battery_level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - value_fn=lambda encharge: encharge.state_of_charge, + value_fn=attrgetter("state_of_charge"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="reserve_soc", translation_key="reserve_soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - value_fn=lambda encharge: encharge.reserve_state_of_charge, + value_fn=attrgetter("reserve_state_of_charge"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="available_energy", translation_key="available_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - value_fn=lambda encharge: encharge.available_energy, + value_fn=attrgetter("available_energy"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="reserve_energy", translation_key="reserve_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - value_fn=lambda encharge: encharge.backup_reserve, + value_fn=attrgetter("backup_reserve"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="max_capacity", translation_key="max_capacity", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - value_fn=lambda encharge: encharge.max_available_capacity, + value_fn=attrgetter("max_available_capacity"), ), ) diff --git a/homeassistant/components/environment_canada/diagnostics.py b/homeassistant/components/environment_canada/diagnostics.py index 63f8bb72189..0fb565fda59 100644 --- a/homeassistant/components/environment_canada/diagnostics.py +++ b/homeassistant/components/environment_canada/diagnostics.py @@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics( coordinators = hass.data[DOMAIN][config_entry.entry_id] weather_coord = coordinators["weather_coordinator"] - diagnostics_data = { + return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "weather_data": dict(weather_coord.ec_data.conditions), } - - return diagnostics_data diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 643e7951c23..a3036c55659 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -222,7 +222,9 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None: forecast_array.append(today) - for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)): + for day, high, low in zip( + range(1, 6), range(0, 9, 2), range(1, 10, 2), strict=False + ): forecast_array.append( { ATTR_FORECAST_TIME: ( diff --git a/homeassistant/components/epic_games_store/__init__.py b/homeassistant/components/epic_games_store/__init__.py new file mode 100644 index 00000000000..af25eb98137 --- /dev/null +++ b/homeassistant/components/epic_games_store/__init__.py @@ -0,0 +1,35 @@ +"""The Epic Games Store integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import EGSCalendarUpdateCoordinator + +PLATFORMS: list[Platform] = [ + Platform.CALENDAR, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Epic Games Store from a config entry.""" + + coordinator = EGSCalendarUpdateCoordinator(hass, entry) + 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 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/epic_games_store/calendar.py b/homeassistant/components/epic_games_store/calendar.py new file mode 100644 index 00000000000..75c448e8467 --- /dev/null +++ b/homeassistant/components/epic_games_store/calendar.py @@ -0,0 +1,97 @@ +"""Calendar platform for a Epic Games Store.""" + +from __future__ import annotations + +from collections import namedtuple +from datetime import datetime +from typing import Any + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, CalendarType +from .coordinator import EGSCalendarUpdateCoordinator + +DateRange = namedtuple("DateRange", ["start", "end"]) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the local calendar platform.""" + coordinator: EGSCalendarUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [ + EGSCalendar(coordinator, entry.entry_id, CalendarType.FREE), + EGSCalendar(coordinator, entry.entry_id, CalendarType.DISCOUNT), + ] + async_add_entities(entities) + + +class EGSCalendar(CoordinatorEntity[EGSCalendarUpdateCoordinator], CalendarEntity): + """A calendar entity by Epic Games Store.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: EGSCalendarUpdateCoordinator, + config_entry_id: str, + cal_type: CalendarType, + ) -> None: + """Initialize EGSCalendar.""" + super().__init__(coordinator) + self._cal_type = cal_type + self._attr_translation_key = f"{cal_type}_games" + self._attr_unique_id = f"{config_entry_id}-{cal_type}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry_id)}, + manufacturer="Epic Games Store", + name="Epic Games Store", + ) + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + if event := self.coordinator.data[self._cal_type]: + return _get_calendar_event(event[0]) + return None + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + events = filter( + lambda game: _are_date_range_overlapping( + DateRange(start=game["discount_start_at"], end=game["discount_end_at"]), + DateRange(start=start_date, end=end_date), + ), + self.coordinator.data[self._cal_type], + ) + return [_get_calendar_event(event) for event in events] + + +def _get_calendar_event(event: dict[str, Any]) -> CalendarEvent: + """Return a CalendarEvent from an API event.""" + return CalendarEvent( + summary=event["title"], + start=event["discount_start_at"], + end=event["discount_end_at"], + description=f"{event['description']}\n\n{event['url']}", + ) + + +def _are_date_range_overlapping(range1: DateRange, range2: DateRange) -> bool: + """Return a CalendarEvent from an API event.""" + latest_start = max(range1.start, range2.start) + earliest_end = min(range1.end, range2.end) + delta = (earliest_end - latest_start).days + 1 + overlap = max(0, delta) + return overlap > 0 diff --git a/homeassistant/components/epic_games_store/config_flow.py b/homeassistant/components/epic_games_store/config_flow.py new file mode 100644 index 00000000000..2ae86060ba2 --- /dev/null +++ b/homeassistant/components/epic_games_store/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for Epic Games Store integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from epicstore_api import EpicGamesStoreAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.selector import ( + CountrySelector, + LanguageSelector, + LanguageSelectorConfig, +) + +from .const import DOMAIN, SUPPORTED_LANGUAGES + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_LANGUAGE): LanguageSelector( + LanguageSelectorConfig(languages=SUPPORTED_LANGUAGES) + ), + vol.Required(CONF_COUNTRY): CountrySelector(), + } +) + + +def get_default_language(hass: HomeAssistant) -> str | None: + """Get default language code based on Home Assistant config.""" + language_code = f"{hass.config.language}-{hass.config.country}" + if language_code in SUPPORTED_LANGUAGES: + return language_code + if hass.config.language in SUPPORTED_LANGUAGES: + return hass.config.language + return None + + +async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None: + """Validate the user input allows us to connect.""" + api = EpicGamesStoreAPI(user_input[CONF_LANGUAGE], user_input[CONF_COUNTRY]) + data = await hass.async_add_executor_job(api.get_free_games) + + if data.get("errors"): + _LOGGER.warning(data["errors"]) + + assert data["data"]["Catalog"]["searchStore"]["elements"] + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Epic Games Store.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + data_schema = self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, + user_input + or { + CONF_LANGUAGE: get_default_language(self.hass), + CONF_COUNTRY: self.hass.config.country, + }, + ) + if user_input is None: + return self.async_show_form(step_id="user", data_schema=data_schema) + + await self.async_set_unique_id( + f"freegames-{user_input[CONF_LANGUAGE]}-{user_input[CONF_COUNTRY]}" + ) + self._abort_if_unique_id_configured() + + errors = {} + + try: + await validate_input(self.hass, user_input) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=f"Epic Games Store - Free Games ({user_input[CONF_LANGUAGE]}-{user_input[CONF_COUNTRY]})", + data=user_input, + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) diff --git a/homeassistant/components/epic_games_store/const.py b/homeassistant/components/epic_games_store/const.py new file mode 100644 index 00000000000..c397698fd0c --- /dev/null +++ b/homeassistant/components/epic_games_store/const.py @@ -0,0 +1,31 @@ +"""Constants for the Epic Games Store integration.""" + +from enum import StrEnum + +DOMAIN = "epic_games_store" + +SUPPORTED_LANGUAGES = [ + "ar", + "de", + "en-US", + "es-ES", + "es-MX", + "fr", + "it", + "ja", + "ko", + "pl", + "pt-BR", + "ru", + "th", + "tr", + "zh-CN", + "zh-Hant", +] + + +class CalendarType(StrEnum): + """Calendar types.""" + + FREE = "free" + DISCOUNT = "discount" diff --git a/homeassistant/components/epic_games_store/coordinator.py b/homeassistant/components/epic_games_store/coordinator.py new file mode 100644 index 00000000000..d9c48f5da02 --- /dev/null +++ b/homeassistant/components/epic_games_store/coordinator.py @@ -0,0 +1,81 @@ +"""The Epic Games Store integration data coordinator.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from epicstore_api import EpicGamesStoreAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, CalendarType +from .helper import format_game_data + +SCAN_INTERVAL = timedelta(days=1) + +_LOGGER = logging.getLogger(__name__) + + +class EGSCalendarUpdateCoordinator( + DataUpdateCoordinator[dict[str, list[dict[str, Any]]]] +): + """Class to manage fetching data from the Epic Game Store.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self._api = EpicGamesStoreAPI( + entry.data[CONF_LANGUAGE], + entry.data[CONF_COUNTRY], + ) + self.language = entry.data[CONF_LANGUAGE] + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, list[dict[str, Any]]]: + """Update data via library.""" + raw_data = await self.hass.async_add_executor_job(self._api.get_free_games) + _LOGGER.debug(raw_data) + data = raw_data["data"]["Catalog"]["searchStore"]["elements"] + + discount_games = filter( + lambda game: game.get("promotions") + and ( + # Current discount(s) + game["promotions"]["promotionalOffers"] + or + # Upcoming discount(s) + game["promotions"]["upcomingPromotionalOffers"] + ), + data, + ) + + return_data: dict[str, list[dict[str, Any]]] = { + CalendarType.DISCOUNT: [], + CalendarType.FREE: [], + } + for discount_game in discount_games: + game = format_game_data(discount_game, self.language) + + if game["discount_type"]: + return_data[game["discount_type"]].append(game) + + return_data[CalendarType.DISCOUNT] = sorted( + return_data[CalendarType.DISCOUNT], + key=lambda game: game["discount_start_at"], + ) + return_data[CalendarType.FREE] = sorted( + return_data[CalendarType.FREE], key=lambda game: game["discount_start_at"] + ) + + _LOGGER.debug(return_data) + return return_data diff --git a/homeassistant/components/epic_games_store/helper.py b/homeassistant/components/epic_games_store/helper.py new file mode 100644 index 00000000000..2510c7699e5 --- /dev/null +++ b/homeassistant/components/epic_games_store/helper.py @@ -0,0 +1,92 @@ +"""Helper for Epic Games Store.""" + +import contextlib +from typing import Any + +from homeassistant.util import dt as dt_util + + +def format_game_data(raw_game_data: dict[str, Any], language: str) -> dict[str, Any]: + """Format raw API game data for Home Assistant users.""" + img_portrait = None + img_landscape = None + + for image in raw_game_data["keyImages"]: + if image["type"] == "OfferImageTall": + img_portrait = image["url"] + if image["type"] == "OfferImageWide": + img_landscape = image["url"] + + current_promotions = raw_game_data["promotions"]["promotionalOffers"] + upcoming_promotions = raw_game_data["promotions"]["upcomingPromotionalOffers"] + + promotion_data = {} + if ( + current_promotions + and raw_game_data["price"]["totalPrice"]["discountPrice"] == 0 + ): + promotion_data = current_promotions[0]["promotionalOffers"][0] + else: + promotion_data = (current_promotions or upcoming_promotions)[0][ + "promotionalOffers" + ][0] + + return { + "title": raw_game_data["title"].replace("\xa0", " "), + "description": raw_game_data["description"].strip().replace("\xa0", " "), + "released_at": dt_util.parse_datetime(raw_game_data["effectiveDate"]), + "original_price": raw_game_data["price"]["totalPrice"]["fmtPrice"][ + "originalPrice" + ].replace("\xa0", " "), + "publisher": raw_game_data["seller"]["name"], + "url": get_game_url(raw_game_data, language), + "img_portrait": img_portrait, + "img_landscape": img_landscape, + "discount_type": ("free" if is_free_game(raw_game_data) else "discount") + if promotion_data + else None, + "discount_start_at": dt_util.parse_datetime(promotion_data["startDate"]) + if promotion_data + else None, + "discount_end_at": dt_util.parse_datetime(promotion_data["endDate"]) + if promotion_data + else None, + } + + +def get_game_url(raw_game_data: dict[str, Any], language: str) -> str: + """Format raw API game data for Home Assistant users.""" + url_bundle_or_product = "bundles" if raw_game_data["offerType"] == "BUNDLE" else "p" + url_slug: str | None = None + try: + url_slug = raw_game_data["offerMappings"][0]["pageSlug"] + except Exception: # pylint: disable=broad-except + with contextlib.suppress(Exception): + url_slug = raw_game_data["catalogNs"]["mappings"][0]["pageSlug"] + + if not url_slug: + url_slug = raw_game_data["urlSlug"] + + return f"https://store.epicgames.com/{language}/{url_bundle_or_product}/{url_slug}" + + +def is_free_game(game: dict[str, Any]) -> bool: + """Return if the game is free or will be free.""" + return ( + # Current free game(s) + game["promotions"]["promotionalOffers"] + and game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0][ + "discountSetting" + ]["discountPercentage"] + == 0 + and + # Checking current price, maybe not necessary + game["price"]["totalPrice"]["discountPrice"] == 0 + ) or ( + # Upcoming free game(s) + game["promotions"]["upcomingPromotionalOffers"] + and game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0][ + "discountSetting" + ]["discountPercentage"] + == 0 + ) diff --git a/homeassistant/components/epic_games_store/manifest.json b/homeassistant/components/epic_games_store/manifest.json new file mode 100644 index 00000000000..665eaec6668 --- /dev/null +++ b/homeassistant/components/epic_games_store/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "epic_games_store", + "name": "Epic Games Store", + "codeowners": ["@hacf-fr", "@Quentame"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/epic_games_store", + "integration_type": "service", + "iot_class": "cloud_polling", + "requirements": ["epicstore-api==0.1.7"] +} diff --git a/homeassistant/components/epic_games_store/strings.json b/homeassistant/components/epic_games_store/strings.json new file mode 100644 index 00000000000..58a87a55f81 --- /dev/null +++ b/homeassistant/components/epic_games_store/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "step": { + "user": { + "data": { + "language": "Language", + "country": "Country" + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "entity": { + "calendar": { + "free_games": { + "name": "Free games", + "state_attributes": { + "games": { + "name": "Games" + } + } + }, + "discount_games": { + "name": "Discount games", + "state_attributes": { + "games": { + "name": "[%key:component::epic_games_store::entity::calendar::free_games::state_attributes::games::name%]" + } + } + } + } + } +} diff --git a/homeassistant/components/epsonworkforce/__init__.py b/homeassistant/components/epsonworkforce/__init__.py deleted file mode 100644 index 5efd217b1dd..00000000000 --- a/homeassistant/components/epsonworkforce/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The epsonworkforce component.""" diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json deleted file mode 100644 index 855689bab7d..00000000000 --- a/homeassistant/components/epsonworkforce/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "epsonworkforce", - "name": "Epson Workforce", - "codeowners": ["@ThaStealth"], - "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", - "iot_class": "local_polling", - "loggers": ["epsonprinter_pkg"], - "requirements": ["epsonprinter==0.0.9"] -} diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py deleted file mode 100644 index dea611a3c3a..00000000000 --- a/homeassistant/components/epsonworkforce/sensor.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Support for Epson Workforce Printer.""" - -from __future__ import annotations - -from datetime import timedelta - -from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="black", - name="Ink level Black", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="photoblack", - name="Ink level Photoblack", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="magenta", - name="Ink level Magenta", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="cyan", - name="Ink level Cyan", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="yellow", - name="Ink level Yellow", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="clean", - name="Cleaning level", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), -) -MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] - ), - } -) -SCAN_INTERVAL = timedelta(minutes=60) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the cartridge sensor.""" - host = config.get(CONF_HOST) - - api = EpsonPrinterAPI(host) - if not api.available: - raise PlatformNotReady - - sensors = [ - EpsonPrinterCartridge(api, description) - for description in SENSOR_TYPES - if description.key in config[CONF_MONITORED_CONDITIONS] - ] - - add_devices(sensors, True) - - -class EpsonPrinterCartridge(SensorEntity): - """Representation of a cartridge sensor.""" - - def __init__( - self, api: EpsonPrinterAPI, description: SensorEntityDescription - ) -> None: - """Initialize a cartridge sensor.""" - self._api = api - self.entity_description = description - - @property - def native_value(self): - """Return the state of the device.""" - return self._api.getSensorValue(self.entity_description.key) - - @property - def available(self) -> bool: - """Could the device be accessed during the last update call.""" - return self._api.available - - def update(self) -> None: - """Get the latest data from the Epson printer.""" - self._api.update() diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py new file mode 100644 index 00000000000..f63e627ea7d --- /dev/null +++ b/homeassistant/components/eq3btsmart/__init__.py @@ -0,0 +1,145 @@ +"""Support for EQ3 devices.""" + +import asyncio +import logging +from typing import TYPE_CHECKING + +from eq3btsmart import Thermostat +from eq3btsmart.exceptions import Eq3Exception +from eq3btsmart.thermostat_config import ThermostatConfig + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import DOMAIN, SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED +from .models import Eq3Config, Eq3ConfigEntryData + +PLATFORMS = [ + Platform.CLIMATE, +] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle config entry setup.""" + + mac_address: str | None = entry.unique_id + + if TYPE_CHECKING: + assert mac_address is not None + + eq3_config = Eq3Config( + mac_address=mac_address, + ) + + device = bluetooth.async_ble_device_from_address( + hass, mac_address.upper(), connectable=True + ) + + if device is None: + raise ConfigEntryNotReady( + f"[{eq3_config.mac_address}] Device could not be found" + ) + + thermostat = Thermostat( + thermostat_config=ThermostatConfig( + mac_address=mac_address, + ), + ble_device=device, + ) + + eq3_config_entry = Eq3ConfigEntryData(eq3_config=eq3_config, thermostat=thermostat) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = eq3_config_entry + + entry.async_on_unload(entry.add_update_listener(update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_create_background_task( + hass, _async_run_thermostat(hass, entry), entry.entry_id + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle config entry unload.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN].pop(entry.entry_id) + await eq3_config_entry.thermostat.async_disconnect() + + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle config entry update.""" + + await hass.config_entries.async_reload(entry.entry_id) + + +async def _async_run_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Run the thermostat.""" + + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id] + thermostat = eq3_config_entry.thermostat + mac_address = eq3_config_entry.eq3_config.mac_address + scan_interval = eq3_config_entry.eq3_config.scan_interval + + await _async_reconnect_thermostat(hass, entry) + + while True: + try: + await thermostat.async_get_status() + except Eq3Exception as e: + if not thermostat.is_connected: + _LOGGER.error( + "[%s] eQ-3 device disconnected", + mac_address, + ) + async_dispatcher_send( + hass, + f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{mac_address}", + ) + await _async_reconnect_thermostat(hass, entry) + continue + + _LOGGER.error( + "[%s] Error updating eQ-3 device: %s", + mac_address, + e, + ) + + await asyncio.sleep(scan_interval) + + +async def _async_reconnect_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reconnect the thermostat.""" + + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id] + thermostat = eq3_config_entry.thermostat + mac_address = eq3_config_entry.eq3_config.mac_address + scan_interval = eq3_config_entry.eq3_config.scan_interval + + while True: + try: + await thermostat.async_connect() + except Eq3Exception: + await asyncio.sleep(scan_interval) + continue + + _LOGGER.debug( + "[%s] eQ-3 device connected", + mac_address, + ) + + async_dispatcher_send( + hass, + f"{SIGNAL_THERMOSTAT_CONNECTED}_{mac_address}", + ) + + return diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py new file mode 100644 index 00000000000..326655d4e59 --- /dev/null +++ b/homeassistant/components/eq3btsmart/climate.py @@ -0,0 +1,306 @@ +"""Platform for eQ-3 climate entities.""" + +import logging +from typing import Any + +from eq3btsmart import Thermostat +from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode +from eq3btsmart.exceptions import Eq3Exception + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceInfo, + async_get, + format_mac, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .const import ( + DEVICE_MODEL, + DOMAIN, + EQ_TO_HA_HVAC, + HA_TO_EQ_HVAC, + MANUFACTURER, + SIGNAL_THERMOSTAT_CONNECTED, + SIGNAL_THERMOSTAT_DISCONNECTED, + CurrentTemperatureSelector, + Preset, + TargetTemperatureSelector, +) +from .entity import Eq3Entity +from .models import Eq3Config, Eq3ConfigEntryData + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Handle config entry setup.""" + + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + [Eq3Climate(eq3_config_entry.eq3_config, eq3_config_entry.thermostat)], + ) + + +class Eq3Climate(Eq3Entity, ClimateEntity): + """Climate entity to represent a eQ-3 thermostat.""" + + _attr_name = None + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_min_temp = EQ3BT_OFF_TEMP + _attr_max_temp = EQ3BT_MAX_TEMP + _attr_precision = PRECISION_HALVES + _attr_hvac_modes = list(HA_TO_EQ_HVAC.keys()) + _attr_preset_modes = list(Preset) + _attr_should_poll = False + _attr_available = False + _attr_hvac_mode: HVACMode | None = None + _attr_hvac_action: HVACAction | None = None + _attr_preset_mode: str | None = None + _target_temperature: float | None = None + + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None: + """Initialize the climate entity.""" + + super().__init__(eq3_config, thermostat) + self._attr_unique_id = format_mac(eq3_config.mac_address) + self._attr_device_info = DeviceInfo( + name=slugify(self._eq3_config.mac_address), + manufacturer=MANUFACTURER, + model=DEVICE_MODEL, + connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, + ) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + + self._thermostat.register_update_callback(self._async_on_updated) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}", + self._async_on_disconnected, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}", + self._async_on_connected, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + + self._thermostat.unregister_update_callback(self._async_on_updated) + + @callback + def _async_on_disconnected(self) -> None: + self._attr_available = False + self.async_write_ha_state() + + @callback + def _async_on_connected(self) -> None: + self._attr_available = True + self.async_write_ha_state() + + @callback + def _async_on_updated(self) -> None: + """Handle updated data from the thermostat.""" + + if self._thermostat.status is not None: + self._async_on_status_updated() + + if self._thermostat.device_data is not None: + self._async_on_device_updated() + + self.async_write_ha_state() + + @callback + def _async_on_status_updated(self) -> None: + """Handle updated status from the thermostat.""" + + self._target_temperature = self._thermostat.status.target_temperature.value + self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode] + self._attr_current_temperature = self._get_current_temperature() + self._attr_target_temperature = self._get_target_temperature() + self._attr_preset_mode = self._get_current_preset_mode() + self._attr_hvac_action = self._get_current_hvac_action() + + @callback + def _async_on_device_updated(self) -> None: + """Handle updated device data from the thermostat.""" + + device_registry = async_get(self.hass) + if device := device_registry.async_get_device( + connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, + ): + device_registry.async_update_device( + device.id, + sw_version=self._thermostat.device_data.firmware_version, + serial_number=self._thermostat.device_data.device_serial.value, + ) + + def _get_current_temperature(self) -> float | None: + """Return the current temperature.""" + + match self._eq3_config.current_temp_selector: + case CurrentTemperatureSelector.NOTHING: + return None + case CurrentTemperatureSelector.VALVE: + if self._thermostat.status is None: + return None + + return float(self._thermostat.status.valve_temperature) + case CurrentTemperatureSelector.UI: + return self._target_temperature + case CurrentTemperatureSelector.DEVICE: + if self._thermostat.status is None: + return None + + return float(self._thermostat.status.target_temperature.value) + case CurrentTemperatureSelector.ENTITY: + state = self.hass.states.get(self._eq3_config.external_temp_sensor) + if state is not None: + try: + return float(state.state) + except ValueError: + pass + + return None + + def _get_target_temperature(self) -> float | None: + """Return the target temperature.""" + + match self._eq3_config.target_temp_selector: + case TargetTemperatureSelector.TARGET: + return self._target_temperature + case TargetTemperatureSelector.LAST_REPORTED: + if self._thermostat.status is None: + return None + + return float(self._thermostat.status.target_temperature.value) + + def _get_current_preset_mode(self) -> str: + """Return the current preset mode.""" + + if (status := self._thermostat.status) is None: + return PRESET_NONE + if status.is_window_open: + return Preset.WINDOW_OPEN + if status.is_boost: + return Preset.BOOST + if status.is_low_battery: + return Preset.LOW_BATTERY + if status.is_away: + return Preset.AWAY + if status.operation_mode is OperationMode.ON: + return Preset.OPEN + if status.presets is None: + return PRESET_NONE + if status.target_temperature == status.presets.eco_temperature: + return Preset.ECO + if status.target_temperature == status.presets.comfort_temperature: + return Preset.COMFORT + + return PRESET_NONE + + def _get_current_hvac_action(self) -> HVACAction: + """Return the current hvac action.""" + + if ( + self._thermostat.status is None + or self._thermostat.status.operation_mode is OperationMode.OFF + ): + return HVACAction.OFF + if self._thermostat.status.valve == 0: + return HVACAction.IDLE + return HVACAction.HEATING + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + + if ATTR_HVAC_MODE in kwargs: + mode: HVACMode | None + if (mode := kwargs.get(ATTR_HVAC_MODE)) is None: + return + + if mode is not HVACMode.OFF: + await self.async_set_hvac_mode(mode) + else: + raise ServiceValidationError( + f"[{self._eq3_config.mac_address}] Can't change HVAC mode to off while changing temperature", + ) + + temperature: float | None + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + + previous_temperature = self._target_temperature + self._target_temperature = temperature + + self.async_write_ha_state() + + try: + await self._thermostat.async_set_temperature(self._target_temperature) + except Eq3Exception: + _LOGGER.error( + "[%s] Failed setting temperature", self._eq3_config.mac_address + ) + self._target_temperature = previous_temperature + self.async_write_ha_state() + except ValueError as ex: + raise ServiceValidationError("Invalid temperature") from ex + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + + if hvac_mode is HVACMode.OFF: + await self.async_set_temperature(temperature=EQ3BT_OFF_TEMP) + + try: + await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode]) + except Eq3Exception: + _LOGGER.error("[%s] Failed setting HVAC mode", self._eq3_config.mac_address) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + + match preset_mode: + case Preset.BOOST: + await self._thermostat.async_set_boost(True) + case Preset.AWAY: + await self._thermostat.async_set_away(True) + case Preset.ECO: + await self._thermostat.async_set_preset(Eq3Preset.ECO) + case Preset.COMFORT: + await self._thermostat.async_set_preset(Eq3Preset.COMFORT) + case Preset.OPEN: + await self._thermostat.async_set_mode(OperationMode.ON) diff --git a/homeassistant/components/eq3btsmart/config_flow.py b/homeassistant/components/eq3btsmart/config_flow.py new file mode 100644 index 00000000000..4dccd8a572a --- /dev/null +++ b/homeassistant/components/eq3btsmart/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow for eQ-3 Bluetooth Smart thermostats.""" + +from typing import Any + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_MAC +from homeassistant.helpers.device_registry import format_mac +from homeassistant.util import slugify + +from .const import DOMAIN +from .schemas import SCHEMA_MAC + + +class EQ3ConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for eQ-3 Bluetooth Smart thermostats.""" + + def __init__(self) -> None: + """Initialize the config flow.""" + + self.mac_address: str = "" + + 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 None: + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_MAC, + errors=errors, + ) + + mac_address = format_mac(user_input[CONF_MAC]) + + if not validate_mac(mac_address): + errors[CONF_MAC] = "invalid_mac_address" + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_MAC, + errors=errors, + ) + + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates=user_input) + + # We can not validate if this mac actually is an eQ-3 thermostat, + # since the thermostat probably is not advertising right now. + return self.async_create_entry(title=slugify(mac_address), data={}) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + """Handle bluetooth discovery.""" + + self.mac_address = format_mac(discovery_info.address) + + await self.async_set_unique_id(self.mac_address) + self._abort_if_unique_id_configured() + + self.context.update({"title_placeholders": {CONF_MAC: self.mac_address}}) + + return await self.async_step_init() + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle flow start.""" + + if user_input is None: + return self.async_show_form( + step_id="init", + description_placeholders={CONF_MAC: self.mac_address}, + ) + + await self.async_set_unique_id(self.mac_address) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=slugify(self.mac_address), + data={}, + ) + + +def validate_mac(mac: str) -> bool: + """Return whether or not given value is a valid MAC address.""" + + return bool( + mac + and len(mac) == 17 + and mac.count(":") == 5 + and all(int(part, 16) < 256 for part in mac.split(":") if part) + ) diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py new file mode 100644 index 00000000000..111c4d0eba4 --- /dev/null +++ b/homeassistant/components/eq3btsmart/const.py @@ -0,0 +1,73 @@ +"""Constants for EQ3 Bluetooth Smart Radiator Valves.""" + +from enum import Enum + +from eq3btsmart.const import OperationMode + +from homeassistant.components.climate import ( + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, + HVACMode, +) + +DOMAIN = "eq3btsmart" + +MANUFACTURER = "eQ-3 AG" +DEVICE_MODEL = "CC-RT-BLE-EQ" + +GET_DEVICE_TIMEOUT = 5 # seconds + + +EQ_TO_HA_HVAC: dict[OperationMode, HVACMode] = { + OperationMode.OFF: HVACMode.OFF, + OperationMode.ON: HVACMode.HEAT, + OperationMode.AUTO: HVACMode.AUTO, + OperationMode.MANUAL: HVACMode.HEAT, +} + +HA_TO_EQ_HVAC = { + HVACMode.OFF: OperationMode.OFF, + HVACMode.AUTO: OperationMode.AUTO, + HVACMode.HEAT: OperationMode.MANUAL, +} + + +class Preset(str, Enum): + """Preset modes for the eQ-3 radiator valve.""" + + NONE = PRESET_NONE + ECO = PRESET_ECO + COMFORT = PRESET_COMFORT + BOOST = PRESET_BOOST + AWAY = PRESET_AWAY + OPEN = "Open" + LOW_BATTERY = "Low Battery" + WINDOW_OPEN = "Window" + + +class CurrentTemperatureSelector(str, Enum): + """Selector for current temperature.""" + + NOTHING = "NOTHING" + UI = "UI" + DEVICE = "DEVICE" + VALVE = "VALVE" + ENTITY = "ENTITY" + + +class TargetTemperatureSelector(str, Enum): + """Selector for target temperature.""" + + TARGET = "TARGET" + LAST_REPORTED = "LAST_REPORTED" + + +DEFAULT_CURRENT_TEMP_SELECTOR = CurrentTemperatureSelector.DEVICE +DEFAULT_TARGET_TEMP_SELECTOR = TargetTemperatureSelector.TARGET +DEFAULT_SCAN_INTERVAL = 10 # seconds + +SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected" +SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected" diff --git a/homeassistant/components/eq3btsmart/entity.py b/homeassistant/components/eq3btsmart/entity.py new file mode 100644 index 00000000000..e8c00d4e3cf --- /dev/null +++ b/homeassistant/components/eq3btsmart/entity.py @@ -0,0 +1,19 @@ +"""Base class for all eQ-3 entities.""" + +from eq3btsmart.thermostat import Thermostat + +from homeassistant.helpers.entity import Entity + +from .models import Eq3Config + + +class Eq3Entity(Entity): + """Base class for all eQ-3 entities.""" + + _attr_has_entity_name = True + + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None: + """Initialize the eq3 entity.""" + + self._eq3_config = eq3_config + self._thermostat = thermostat diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json new file mode 100644 index 00000000000..6c4a59962ff --- /dev/null +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -0,0 +1,27 @@ +{ + "domain": "eq3btsmart", + "name": "eQ-3 Bluetooth Smart Thermostats", + "bluetooth": [ + { + "local_name": "CC-RT-BLE", + "connectable": true + }, + { + "local_name": "CC-RT-M-BLE", + "connectable": true + }, + { + "local_name": "CC-RT-BLE-EQ", + "connectable": true + } + ], + "codeowners": ["@eulemitkeule", "@dbuezas"], + "config_flow": true, + "dependencies": ["bluetooth", "bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", + "integration_type": "device", + "iot_class": "local_polling", + "loggers": ["eq3btsmart"], + "quality_scale": "silver", + "requirements": ["eq3btsmart==1.1.6", "bleak-esphome==1.0.0"] +} diff --git a/homeassistant/components/eq3btsmart/models.py b/homeassistant/components/eq3btsmart/models.py new file mode 100644 index 00000000000..8ea0955dbdd --- /dev/null +++ b/homeassistant/components/eq3btsmart/models.py @@ -0,0 +1,35 @@ +"""Models for eq3btsmart integration.""" + +from dataclasses import dataclass + +from eq3btsmart.const import DEFAULT_AWAY_HOURS, DEFAULT_AWAY_TEMP +from eq3btsmart.thermostat import Thermostat + +from .const import ( + DEFAULT_CURRENT_TEMP_SELECTOR, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TARGET_TEMP_SELECTOR, + CurrentTemperatureSelector, + TargetTemperatureSelector, +) + + +@dataclass(slots=True) +class Eq3Config: + """Config for a single eQ-3 device.""" + + mac_address: str + current_temp_selector: CurrentTemperatureSelector = DEFAULT_CURRENT_TEMP_SELECTOR + target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR + external_temp_sensor: str = "" + scan_interval: int = DEFAULT_SCAN_INTERVAL + default_away_hours: float = DEFAULT_AWAY_HOURS + default_away_temperature: float = DEFAULT_AWAY_TEMP + + +@dataclass(slots=True) +class Eq3ConfigEntryData: + """Config entry for a single eQ-3 device.""" + + eq3_config: Eq3Config + thermostat: Thermostat diff --git a/homeassistant/components/eq3btsmart/schemas.py b/homeassistant/components/eq3btsmart/schemas.py new file mode 100644 index 00000000000..643bb4a02a6 --- /dev/null +++ b/homeassistant/components/eq3btsmart/schemas.py @@ -0,0 +1,15 @@ +"""Voluptuous schemas for eq3btsmart.""" + +from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_MIN_TEMP +import voluptuous as vol + +from homeassistant.const import CONF_MAC +from homeassistant.helpers import config_validation as cv + +SCHEMA_TEMPERATURE = vol.Range(min=EQ3BT_MIN_TEMP, max=EQ3BT_MAX_TEMP) +SCHEMA_DEVICE = vol.Schema({vol.Required(CONF_MAC): cv.string}) +SCHEMA_MAC = vol.Schema( + { + vol.Required(CONF_MAC): str, + } +) diff --git a/homeassistant/components/eq3btsmart/strings.json b/homeassistant/components/eq3btsmart/strings.json new file mode 100644 index 00000000000..7477aab4cfb --- /dev/null +++ b/homeassistant/components/eq3btsmart/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "flow_title": "eQ-3 Device [{mac}]", + "step": { + "user": { + "title": "Configure new eQ-3 device", + "data": { + "mac": "MAC address" + } + }, + "init": { + "title": "Configure new eQ-3 device" + } + } + } +} diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index ac0676d8d1e..05ddfc2c43f 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -33,7 +33,9 @@ async def async_setup_entry( entry_data = DomainData.get(hass).get_entry_data(entry) assert entry_data.device_info is not None - if entry_data.device_info.voice_assistant_version: + if entry_data.device_info.voice_assistant_feature_flags_compat( + entry_data.api_version + ): async_add_entities([EsphomeAssistInProgressBinarySensor(entry_data)]) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 5e166db7092..67e94121e1d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -430,8 +430,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): except aiohttp.ClientError as err: _LOGGER.error("Error talking to the dashboard: %s", err) return False - except json.JSONDecodeError as err: - _LOGGER.exception("Error parsing response from dashboard: %s", err) + except json.JSONDecodeError: + _LOGGER.exception("Error parsing response from dashboard") return False self._noise_psk = noise_psk diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index b8a72ac4398..54a593fe0cc 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -103,7 +103,7 @@ class ESPHomeDashboardManager: await dashboard.async_shutdown() self._cancel_shutdown = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, on_hass_stop, run_immediately=True + EVENT_HOMEASSISTANT_STOP, on_hass_stop ) new_data = {"info": {"addon_slug": addon_slug, "host": host, "port": port}} diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py new file mode 100644 index 00000000000..15509a46158 --- /dev/null +++ b/homeassistant/components/esphome/datetime.py @@ -0,0 +1,48 @@ +"""Support for esphome datetimes.""" + +from __future__ import annotations + +from datetime import datetime + +from aioesphomeapi import DateTimeInfo, DateTimeState + +from homeassistant.components.datetime import DateTimeEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util + +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 datetimes based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + info_type=DateTimeInfo, + entity_type=EsphomeDateTime, + state_type=DateTimeState, + ) + + +class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity): + """A datetime implementation for esphome.""" + + @property + @esphome_state_property + def native_value(self) -> datetime | None: + """Return the state of the entity.""" + state = self._state + if state.missing_state: + return None + return dt_util.utc_from_timestamp(state.epoch_seconds) + + async def async_set_value(self, value: datetime) -> None: + """Update the current datetime.""" + self._client.datetime_command(self._key, int(value.timestamp())) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index da0dae52569..41b18c9b88c 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -20,9 +20,12 @@ from aioesphomeapi import ( ClimateInfo, CoverInfo, DateInfo, + DateTimeInfo, DeviceInfo, EntityInfo, EntityState, + Event, + EventInfo, FanInfo, LightInfo, LockInfo, @@ -36,6 +39,7 @@ from aioesphomeapi import ( TextSensorInfo, TimeInfo, UserService, + ValveInfo, build_unique_id, ) from aioesphomeapi.model import ButtonInfo @@ -45,9 +49,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform 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 @@ -67,6 +69,8 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, DateInfo: Platform.DATE, + DateTimeInfo: Platform.DATETIME, + EventInfo: Platform.EVENT, FanInfo: Platform.FAN, LightInfo: Platform.LIGHT, LockInfo: Platform.LOCK, @@ -78,6 +82,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { TextInfo: Platform.TEXT, TextSensorInfo: Platform.SENSOR, TimeInfo: Platform.TIME, + ValveInfo: Platform.VALVE, } @@ -119,6 +124,9 @@ class RuntimeEntryData: default_factory=dict ) device_update_subscriptions: set[CALLBACK_TYPE] = field(default_factory=set) + static_info_update_subscriptions: set[Callable[[list[EntityInfo]], None]] = 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 @@ -147,11 +155,6 @@ class RuntimeEntryData: "_", " " ) - @property - def signal_static_info_updated(self) -> SignalType[list[EntityInfo]]: - """Return the signal to listen to for updates on static info.""" - return SignalType(f"esphome_{self.entry_id}_on_list") - @callback def async_register_static_info_callback( self, @@ -257,7 +260,9 @@ class RuntimeEntryData: if async_get_dashboard(hass): needed_platforms.add(Platform.UPDATE) - if self.device_info and self.device_info.voice_assistant_version: + if self.device_info and self.device_info.voice_assistant_feature_flags_compat( + self.api_version + ): needed_platforms.add(Platform.BINARY_SENSOR) needed_platforms.add(Platform.SELECT) @@ -294,8 +299,9 @@ class RuntimeEntryData: for callback_ in callbacks_: callback_(entity_infos) - # Then send dispatcher event - async_dispatcher_send(hass, self.signal_static_info_updated, infos) + # Finally update static info subscriptions + for callback_ in self.static_info_update_subscriptions: + callback_(infos) @callback def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE: @@ -308,6 +314,21 @@ class RuntimeEntryData: """Unsubscribe to device updates.""" self.device_update_subscriptions.remove(callback_) + @callback + def async_subscribe_static_info_updated( + self, callback_: Callable[[list[EntityInfo]], None] + ) -> CALLBACK_TYPE: + """Subscribe to static info updates.""" + self.static_info_update_subscriptions.add(callback_) + return partial(self._async_unsubscribe_static_info_updated, callback_) + + @callback + def _async_unsubscribe_static_info_updated( + self, callback_: Callable[[list[EntityInfo]], None] + ) -> None: + """Unsubscribe to static info updates.""" + self.static_info_update_subscriptions.remove(callback_) + @callback def async_subscribe_state_update( self, @@ -339,7 +360,7 @@ class RuntimeEntryData: if ( current_state == state and subscription_key not in stale_state - and state_type is not CameraState + and state_type not in (CameraState, Event) and not ( state_type is SensorState and (platform_info := self.info.get(SensorInfo)) @@ -353,11 +374,11 @@ class RuntimeEntryData: if subscription := self.state_subscriptions.get(subscription_key): try: subscription() - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except # If we allow this exception to raise it will # make it all the way to data_received in aioesphomeapi # which will cause the connection to be closed. - _LOGGER.exception("Error while calling subscription: %s", ex) + _LOGGER.exception("Error while calling subscription") @callback def async_update_device_state(self) -> None: diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py new file mode 100644 index 00000000000..3c7331beba0 --- /dev/null +++ b/homeassistant/components/esphome/event.py @@ -0,0 +1,48 @@ +"""Support for ESPHome event components.""" + +from __future__ import annotations + +from aioesphomeapi import EntityInfo, Event, EventInfo + +from homeassistant.components.event import EventDeviceClass, EventEntity +from homeassistant.config_entries import ConfigEntry +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 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ESPHome event based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + info_type=EventInfo, + entity_type=EsphomeEvent, + state_type=Event, + ) + + +class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): + """An event implementation for ESPHome.""" + + @callback + def _on_static_info_update(self, static_info: EntityInfo) -> None: + """Set attrs from static info.""" + super()._on_static_info_update(static_info) + static_info = self._static_info + if event_types := static_info.event_types: + self._attr_event_types = event_types + self._attr_device_class = try_parse_enum( + EventDeviceClass, static_info.device_class + ) + + @callback + def _on_state_update(self) -> None: + self._update_state_from_entry_data() + self._trigger_event(self._state.event_type) + self.async_write_ha_state() diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index dc95952194e..ef56f3a2164 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -21,6 +21,7 @@ from aioesphomeapi import ( UserService, UserServiceArgType, VoiceAssistantAudioSettings, + VoiceAssistantFeature, ) from awesomeversion import AwesomeVersion import voluptuous as vol @@ -33,16 +34,20 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_CLOSE, EVENT_LOGGING_CHANGED, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -68,7 +73,11 @@ from .domain_data import DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData -from .voice_assistant import VoiceAssistantUDPServer +from .voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantPipeline, + VoiceAssistantUDPPipeline, +) _LOGGER = logging.getLogger(__name__) @@ -139,7 +148,7 @@ class ESPHomeManager: "cli", "device_id", "domain_data", - "voice_assistant_udp_server", + "voice_assistant_pipeline", "reconnect_logic", "zeroconf_instance", "entry_data", @@ -164,13 +173,13 @@ class ESPHomeManager: self.cli = cli self.device_id: str | None = None self.domain_data = domain_data - self.voice_assistant_udp_server: VoiceAssistantUDPServer | None = None + self.voice_assistant_pipeline: VoiceAssistantPipeline | None = None self.reconnect_logic: ReconnectLogic | None = None self.zeroconf_instance = zeroconf_instance self.entry_data = entry_data async def on_stop(self, event: Event) -> None: - """Cleanup the socket client on HA stop.""" + """Cleanup the socket client on HA close.""" await cleanup_instance(self.hass, self.entry) @property @@ -323,9 +332,10 @@ class ESPHomeManager: def _handle_pipeline_finished(self) -> None: self.entry_data.async_set_assist_pipeline_state(False) - if self.voice_assistant_udp_server is not None: - self.voice_assistant_udp_server.close() - self.voice_assistant_udp_server = None + if self.voice_assistant_pipeline is not None: + if isinstance(self.voice_assistant_pipeline, VoiceAssistantUDPPipeline): + self.voice_assistant_pipeline.close() + self.voice_assistant_pipeline = None async def _handle_pipeline_start( self, @@ -335,38 +345,60 @@ class ESPHomeManager: wake_word_phrase: str | None, ) -> int | None: """Start a voice assistant pipeline.""" - if self.voice_assistant_udp_server is not None: + if self.voice_assistant_pipeline is not None: _LOGGER.warning("Voice assistant UDP server was not stopped") - self.voice_assistant_udp_server.stop() - self.voice_assistant_udp_server = None + self.voice_assistant_pipeline.stop() + self.voice_assistant_pipeline = None hass = self.hass - self.voice_assistant_udp_server = VoiceAssistantUDPServer( - hass, - self.entry_data, - self.cli.send_voice_assistant_event, - self._handle_pipeline_finished, - ) - port = await self.voice_assistant_udp_server.start_server() + assert self.entry_data.device_info is not None + if ( + self.entry_data.device_info.voice_assistant_feature_flags_compat( + self.entry_data.api_version + ) + & VoiceAssistantFeature.API_AUDIO + ): + self.voice_assistant_pipeline = VoiceAssistantAPIPipeline( + hass, + self.entry_data, + self.cli.send_voice_assistant_event, + self._handle_pipeline_finished, + self.cli, + ) + port = 0 + else: + self.voice_assistant_pipeline = VoiceAssistantUDPPipeline( + hass, + self.entry_data, + self.cli.send_voice_assistant_event, + self._handle_pipeline_finished, + ) + port = await self.voice_assistant_pipeline.start_server() assert self.device_id is not None, "Device ID must be set" hass.async_create_background_task( - self.voice_assistant_udp_server.run_pipeline( + self.voice_assistant_pipeline.run_pipeline( device_id=self.device_id, conversation_id=conversation_id or None, flags=flags, audio_settings=audio_settings, wake_word_phrase=wake_word_phrase, ), - "esphome.voice_assistant_udp_server.run_pipeline", + "esphome.voice_assistant_pipeline.run_pipeline", ) return port async def _handle_pipeline_stop(self) -> None: """Stop a voice assistant pipeline.""" - if self.voice_assistant_udp_server is not None: - self.voice_assistant_udp_server.stop() + if self.voice_assistant_pipeline is not None: + self.voice_assistant_pipeline.stop() + + async def _handle_audio(self, data: bytes) -> None: + if self.voice_assistant_pipeline is None: + return + assert isinstance(self.voice_assistant_pipeline, VoiceAssistantAPIPipeline) + self.voice_assistant_pipeline.receive_audio_bytes(data) async def on_connect(self) -> None: """Subscribe to states and list entities on successful API login.""" @@ -468,13 +500,23 @@ class ESPHomeManager: ) ) - if device_info.voice_assistant_version: - entry_data.disconnect_callbacks.add( - cli.subscribe_voice_assistant( - self._handle_pipeline_start, - self._handle_pipeline_stop, + flags = device_info.voice_assistant_feature_flags_compat(api_version) + if flags: + if flags & VoiceAssistantFeature.API_AUDIO: + entry_data.disconnect_callbacks.add( + cli.subscribe_voice_assistant( + handle_start=self._handle_pipeline_start, + handle_stop=self._handle_pipeline_stop, + handle_audio=self._handle_audio, + ) + ) + else: + entry_data.disconnect_callbacks.add( + cli.subscribe_voice_assistant( + handle_start=self._handle_pipeline_start, + handle_stop=self._handle_pipeline_stop, + ) ) - ) cli.subscribe_states(entry_data.async_update_state) cli.subscribe_service_calls(self.async_on_service_call) @@ -546,15 +588,12 @@ class ESPHomeManager: # 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_CLOSE, self.on_stop, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_CLOSE, self.on_stop) ) entry_data.cleanup_callbacks.append( hass.bus.async_listen( EVENT_LOGGING_CHANGED, self._async_handle_logging_changed, - run_immediately=True, ) ) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index f1a5333c403..cde44fa3231 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==23.2.0", + "aioesphomeapi==24.3.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 07a9d70e558..612ffc4bcc6 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -42,7 +42,9 @@ async def async_setup_entry( entry_data = DomainData.get(hass).get_entry_data(entry) assert entry_data.device_info is not None - if entry_data.device_info.voice_assistant_version: + if entry_data.device_info.voice_assistant_feature_flags_compat( + entry_data.api_version + ): async_add_entities( [ EsphomeAssistPipelineSelect(hass, entry_data), diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 3e5a82bbd0b..b16a6e798b7 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -17,7 +17,6 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr 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.update_coordinator import CoordinatorEntity @@ -149,14 +148,9 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): async def async_added_to_hass(self) -> None: """Handle entity added to Home Assistant.""" await super().async_added_to_hass() - hass = self.hass entry_data = self._entry_data self.async_on_remove( - async_dispatcher_connect( - hass, - entry_data.signal_static_info_updated, - self._handle_device_update, - ) + entry_data.async_subscribe_static_info_updated(self._handle_device_update) ) self.async_on_remove( entry_data.async_subscribe_device_updated(self._handle_device_update) diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py new file mode 100644 index 00000000000..5798d38803f --- /dev/null +++ b/homeassistant/components/esphome/valve.py @@ -0,0 +1,103 @@ +"""Support for ESPHome valves.""" + +from __future__ import annotations + +from typing import Any + +from aioesphomeapi import EntityInfo, ValveInfo, ValveOperation, ValveState + +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +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, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ESPHome valves based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + info_type=ValveInfo, + entity_type=EsphomeValve, + state_type=ValveState, + ) + + +class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): + """A valve implementation for ESPHome.""" + + @callback + def _on_static_info_update(self, static_info: EntityInfo) -> None: + """Set attrs from static info.""" + super()._on_static_info_update(static_info) + static_info = self._static_info + flags = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + if static_info.supports_stop: + flags |= ValveEntityFeature.STOP + if static_info.supports_position: + flags |= ValveEntityFeature.SET_POSITION + self._attr_supported_features = flags + self._attr_device_class = try_parse_enum( + ValveDeviceClass, static_info.device_class + ) + self._attr_assumed_state = static_info.assumed_state + self._attr_reports_position = static_info.supports_position + + @property + @esphome_state_property + def is_closed(self) -> bool: + """Return if the valve is closed or not.""" + return self._state.position == 0.0 + + @property + @esphome_state_property + def is_opening(self) -> bool: + """Return if the valve is opening or not.""" + return self._state.current_operation is ValveOperation.IS_OPENING + + @property + @esphome_state_property + def is_closing(self) -> bool: + """Return if the valve is closing or not.""" + return self._state.current_operation is ValveOperation.IS_CLOSING + + @property + @esphome_state_property + def current_valve_position(self) -> int | None: + """Return current position of valve. 0 is closed, 100 is open.""" + return round(self._state.position * 100.0) + + @convert_api_error_ha_error + async def async_open_valve(self, **kwargs: Any) -> None: + """Open the valve.""" + self._client.valve_command(key=self._key, position=1.0) + + @convert_api_error_ha_error + async def async_close_valve(self, **kwargs: Any) -> None: + """Close valve.""" + self._client.valve_command(key=self._key, position=0.0) + + @convert_api_error_ha_error + async def async_stop_valve(self, **kwargs: Any) -> None: + """Stop the valve.""" + self._client.valve_command(key=self._key, stop=True) + + @convert_api_error_ha_error + async def async_set_valve_position(self, position: float) -> None: + """Move the valve to a specific position.""" + self._client.valve_command(key=self._key, position=position / 100) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index f856cc27179..f9f753389ed 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -11,9 +11,11 @@ from typing import cast import wave from aioesphomeapi import ( + APIClient, VoiceAssistantAudioSettings, VoiceAssistantCommandFlag, VoiceAssistantEventType, + VoiceAssistantFeature, ) from homeassistant.components import stt, tts @@ -64,13 +66,11 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[ ) -class VoiceAssistantUDPServer(asyncio.DatagramProtocol): - """Receive UDP packets and forward them to the voice assistant.""" +class VoiceAssistantPipeline: + """Base abstract pipeline class.""" started = False stop_requested = False - transport: asyncio.DatagramTransport | None = None - remote_addr: tuple[str, int] | None = None def __init__( self, @@ -79,12 +79,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], handle_finished: Callable[[], None], ) -> None: - """Initialize UDP receiver.""" + """Initialize the pipeline.""" self.context = Context() self.hass = hass - - assert entry_data.device_info is not None self.entry_data = entry_data + assert entry_data.device_info is not None self.device_info = entry_data.device_info self.queue: asyncio.Queue[bytes] = asyncio.Queue() @@ -95,69 +94,9 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): @property def is_running(self) -> bool: - """True if the UDP server is started and hasn't been asked to stop.""" + """True if the pipeline is started and hasn't been asked to stop.""" return self.started and (not self.stop_requested) - async def start_server(self) -> int: - """Start accepting connections.""" - - def accept_connection() -> VoiceAssistantUDPServer: - """Accept connection.""" - if self.started: - raise RuntimeError("Can only start once") - if self.stop_requested: - raise RuntimeError("No longer accepting connections") - - self.started = True - return self - - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setblocking(False) - - sock.bind(("", UDP_PORT)) - - await asyncio.get_running_loop().create_datagram_endpoint( - accept_connection, sock=sock - ) - - return cast(int, sock.getsockname()[1]) - - @callback - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """Store transport for later use.""" - self.transport = cast(asyncio.DatagramTransport, transport) - - @callback - def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: - """Handle incoming UDP packet.""" - if not self.is_running: - return - if self.remote_addr is None: - self.remote_addr = addr - self.queue.put_nowait(data) - - def error_received(self, exc: Exception) -> None: - """Handle when a send or receive operation raises an OSError. - - (Other than BlockingIOError or InterruptedError.) - """ - _LOGGER.error("ESPHome Voice Assistant UDP server error received: %s", exc) - self.handle_finished() - - @callback - def stop(self) -> None: - """Stop the receiver.""" - self.queue.put_nowait(b"") - self.close() - - def close(self) -> None: - """Close the receiver.""" - self.started = False - self.stop_requested = True - - if self.transport is not None: - self.transport.close() - async def _iterate_packets(self) -> AsyncIterable[bytes]: """Iterate over incoming packets.""" while data := await self.queue.get(): @@ -198,7 +137,12 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): url = async_process_play_media_url(self.hass, path) data_to_send = {"url": url} - if self.device_info.voice_assistant_version >= 2: + if ( + self.device_info.voice_assistant_feature_flags_compat( + self.entry_data.api_version + ) + & VoiceAssistantFeature.SPEAKER + ): media_id = tts_output["media_id"] self._tts_task = self.hass.async_create_background_task( self._send_tts(media_id), "esphome_voice_assistant_tts" @@ -243,9 +187,15 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): if audio_settings is None or audio_settings.volume_multiplier == 0: audio_settings = VoiceAssistantAudioSettings() - tts_audio_output = ( - "wav" if self.device_info.voice_assistant_version >= 2 else "mp3" - ) + if ( + self.device_info.voice_assistant_feature_flags_compat( + self.entry_data.api_version + ) + & VoiceAssistantFeature.SPEAKER + ): + tts_audio_output = "wav" + else: + tts_audio_output = "mp3" _LOGGER.debug("Starting pipeline") if flags & VoiceAssistantCommandFlag.USE_WAKE_WORD: @@ -315,7 +265,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): self.handle_event(VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}) try: - if (not self.is_running) or (self.transport is None): + if not self.is_running: return extension, data = await tts.async_get_media_source_audio( @@ -358,16 +308,133 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): samples_in_chunk = len(chunk) // bytes_per_sample samples_left -= samples_in_chunk - self.transport.sendto(chunk, self.remote_addr) + self.send_audio_bytes(chunk) await asyncio.sleep( samples_in_chunk / stt.AudioSampleRates.SAMPLERATE_16000 * 0.9 ) sample_offset += samples_in_chunk - finally: self.handle_event( VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_END, {} ) self._tts_task = None self._tts_done.set() + + def send_audio_bytes(self, data: bytes) -> None: + """Send bytes to the device.""" + raise NotImplementedError + + def stop(self) -> None: + """Stop the pipeline.""" + self.queue.put_nowait(b"") + + +class VoiceAssistantUDPPipeline(asyncio.DatagramProtocol, VoiceAssistantPipeline): + """Receive UDP packets and forward them to the voice assistant.""" + + transport: asyncio.DatagramTransport | None = None + remote_addr: tuple[str, int] | None = None + + async def start_server(self) -> int: + """Start accepting connections.""" + + def accept_connection() -> VoiceAssistantUDPPipeline: + """Accept connection.""" + if self.started: + raise RuntimeError("Can only start once") + if self.stop_requested: + raise RuntimeError("No longer accepting connections") + + self.started = True + return self + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(False) + + sock.bind(("", UDP_PORT)) + + await asyncio.get_running_loop().create_datagram_endpoint( + accept_connection, sock=sock + ) + + return cast(int, sock.getsockname()[1]) + + @callback + def connection_made(self, transport: asyncio.BaseTransport) -> None: + """Store transport for later use.""" + self.transport = cast(asyncio.DatagramTransport, transport) + + @callback + def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: + """Handle incoming UDP packet.""" + if not self.is_running: + return + if self.remote_addr is None: + self.remote_addr = addr + self.queue.put_nowait(data) + + def error_received(self, exc: Exception) -> None: + """Handle when a send or receive operation raises an OSError. + + (Other than BlockingIOError or InterruptedError.) + """ + _LOGGER.error("ESPHome Voice Assistant UDP server error received: %s", exc) + self.handle_finished() + + @callback + def stop(self) -> None: + """Stop the receiver.""" + super().stop() + self.close() + + def close(self) -> None: + """Close the receiver.""" + self.started = False + self.stop_requested = True + + if self.transport is not None: + self.transport.close() + + def send_audio_bytes(self, data: bytes) -> None: + """Send bytes to the device via UDP.""" + if self.transport is None: + _LOGGER.error("No transport to send audio to") + return + self.transport.sendto(data, self.remote_addr) + + +class VoiceAssistantAPIPipeline(VoiceAssistantPipeline): + """Send audio to the voice assistant via the API.""" + + def __init__( + self, + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + api_client: APIClient, + ) -> None: + """Initialize the pipeline.""" + super().__init__(hass, entry_data, handle_event, handle_finished) + self.api_client = api_client + self.started = True + + def send_audio_bytes(self, data: bytes) -> None: + """Send bytes to the device via the API.""" + self.api_client.send_voice_assistant_audio(data) + + @callback + def receive_audio_bytes(self, data: bytes) -> None: + """Receive audio bytes from the device.""" + if not self.is_running: + return + self.queue.put_nowait(data) + + @callback + def stop(self) -> None: + """Stop the pipeline.""" + super().stop() + + self.started = False + self.stop_requested = True diff --git a/homeassistant/components/eufylife_ble/strings.json b/homeassistant/components/eufylife_ble/strings.json index aaeeeb85f67..72f0e7b5973 100644 --- a/homeassistant/components/eufylife_ble/strings.json +++ b/homeassistant/components/eufylife_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 2ad0e6d950b..925c0855c71 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -5,8 +5,9 @@ from __future__ import annotations from dataclasses import asdict, dataclass from datetime import datetime, timedelta from enum import StrEnum +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Self, final +from typing import Any, Self, final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -22,12 +23,6 @@ from homeassistant.util import dt as dt_util from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 3017685a307..4564e863e42 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -19,7 +19,10 @@ from evohomeasync2.schema.const import ( SZ_ALLOWED_SYSTEM_MODES, SZ_AUTO_WITH_RESET, SZ_CAN_BE_TEMPORARY, + SZ_GATEWAY_ID, + SZ_GATEWAY_INFO, SZ_HEAT_SETPOINT, + SZ_LOCATION_ID, SZ_LOCATION_INFO, SZ_SETPOINT_STATUS, SZ_STATE_STATUS, @@ -30,7 +33,7 @@ from evohomeasync2.schema.const import ( SZ_TIMING_MODE, SZ_UNTIL, ) -import voluptuous as vol # type: ignore[import-untyped] +import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, @@ -261,14 +264,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return False if _LOGGER.isEnabledFor(logging.DEBUG): - _config: dict[str, Any] = { - SZ_LOCATION_INFO: {SZ_TIME_ZONE: None}, - GWS: [{TCS: None}], + loc_info = { + SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], + SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], + } + gwy_info = { + SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], + TCS: loc_config[GWS][0][TCS], + } + _config = { + SZ_LOCATION_INFO: loc_info, + GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], } - _config[SZ_LOCATION_INFO][SZ_TIME_ZONE] = loc_config[SZ_LOCATION_INFO][ - SZ_TIME_ZONE - ] - _config[GWS][0][TCS] = loc_config[GWS][0][TCS] _LOGGER.debug("Config = %s", _config) client_v1 = ev1.EvohomeClient( @@ -455,7 +462,7 @@ class EvoBroker: self.client.access_token_expires # type: ignore[arg-type] ) - app_storage = { + app_storage: dict[str, Any] = { CONF_USERNAME: self.client.username, REFRESH_TOKEN: self.client.refresh_token, ACCESS_TOKEN: self.client.access_token, @@ -463,11 +470,11 @@ class EvoBroker: } if self.client_v1: - app_storage[USER_DATA] = { # type: ignore[assignment] + app_storage[USER_DATA] = { SZ_SESSION_ID: self.client_v1.broker.session_id, } # this is the schema for STORAGE_VER == 1 else: - app_storage[USER_DATA] = {} # type: ignore[assignment] + app_storage[USER_DATA] = {} await self._store.async_save(app_storage) diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index a453398a17a..a17d8312700 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -70,15 +70,13 @@ def _validate_and_create_auth(data: dict) -> dict[str, Any]: ezviz_token = ezviz_client.login() - auth_data = { + return { CONF_SESSION_ID: ezviz_token[CONF_SESSION_ID], CONF_RFSESSION_ID: ezviz_token[CONF_RFSESSION_ID], CONF_URL: ezviz_token["api_url"], CONF_TYPE: ATTR_TYPE_CLOUD, } - return auth_data - def _test_camera_rtsp_creds(data: dict) -> None: """Try DESCRIBE on RTSP camera with credentials.""" diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py index 5a42c9f7602..935831c467d 100644 --- a/homeassistant/components/faa_delays/config_flow.py +++ b/homeassistant/components/faa_delays/config_flow.py @@ -43,8 +43,8 @@ class FAADelaysConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Error connecting to FAA API") errors["base"] = "cannot_connect" - except Exception as error: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", error) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index b62f207bde8..0bed3eb1ff2 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -5,9 +5,10 @@ from __future__ import annotations from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging import math -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -40,12 +41,6 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) DOMAIN = "fan" diff --git a/homeassistant/components/fastdotcom/config_flow.py b/homeassistant/components/fastdotcom/config_flow.py index ec62c86d787..36b6f81ae5b 100644 --- a/homeassistant/components/fastdotcom/config_flow.py +++ b/homeassistant/components/fastdotcom/config_flow.py @@ -20,9 +20,6 @@ class FastdotcomConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - 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={}) diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 02fd3ade205..9e2e077858c 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -6,5 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "iot_class": "cloud_polling", "loggers": ["fastdotcom"], - "requirements": ["fastdotcom==0.0.3"] + "quality_scale": "gold", + "requirements": ["fastdotcom==0.0.3"], + "single_config_entry": true } diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 0a16e986d0b..2b0c6b77559 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -117,7 +117,7 @@ class FeedManager: def _update(self) -> struct_time | None: """Update the feed and publish new entries to the event bus.""" _LOGGER.debug("Fetching new data from feed %s", self._url) - self._feed: feedparser.FeedParserDict = feedparser.parse( # type: ignore[no-redef] + self._feed = feedparser.parse( self._url, etag=None if not self._feed else self._feed.get("etag"), modified=None if not self._feed else self._feed.get("modified"), diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 2045b6bb06b..e5086166ff5 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations import asyncio +from functools import cached_property import re -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import Generic, TypeVar from haffmpeg.core import HAFFmpeg from haffmpeg.tools import IMAGE_JPEG, FFVersion, ImageFrame @@ -19,7 +20,6 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) @@ -27,12 +27,7 @@ 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 - +from homeassistant.util.signal_type import SignalType _HAFFmpegT = TypeVar("_HAFFmpegT", bound=HAFFmpeg) @@ -138,10 +133,9 @@ async def async_get_image( else: extra_cmd += " " + size_cmd - image = await asyncio.shield( + return await asyncio.shield( ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd) ) - return image class FFmpegManager: diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 2c1405130b4..5b7908ddf08 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -108,26 +108,21 @@ class FibaroController: # Device infos by fibaro device id self._device_infos: dict[int, DeviceInfo] = {} - def connect(self) -> bool: + def connect(self) -> None: """Start the communication with the Fibaro controller.""" - connected = self._client.connect() + # Return value doesn't need to be checked, + # it is only relevant when connecting without credentials + self._client.connect() info = self._client.read_info() self.hub_serial = info.serial_number self.hub_name = info.hc_name self.hub_model = info.platform self.hub_software_version = info.current_version - if connected is False: - _LOGGER.error( - "Invalid login for Fibaro HC. Please check username and password" - ) - return False - self._room_map = {room.fibaro_id: room for room in self._client.read_rooms()} self._read_devices() self._scenes = self._client.read_scenes() - return True def connect_with_error_handling(self) -> None: """Translate connect errors to easily differentiate auth and connect failures. @@ -135,9 +130,7 @@ class FibaroController: When there is a better error handling in the used library this can be improved. """ try: - connected = self.connect() - if not connected: - raise FibaroConnectFailed("Connect status is false") + self.connect() except HTTPError as http_ex: if http_ex.response.status_code == 403: raise FibaroAuthFailed from http_ex @@ -382,7 +375,7 @@ class FibaroController: pass -def _init_controller(data: Mapping[str, Any]) -> FibaroController: +def init_controller(data: Mapping[str, Any]) -> FibaroController: """Validate the user input allows us to connect to fibaro.""" controller = FibaroController(data) controller.connect_with_error_handling() @@ -395,7 +388,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: The unique id of the config entry is the serial number of the home center. """ try: - controller = await hass.async_add_executor_job(_init_controller, entry.data) + controller = await hass.async_add_executor_job(init_controller, entry.data) except FibaroConnectFailed as connect_ex: raise ConfigEntryNotReady( f"Could not connect to controller at {entry.data[CONF_URL]}" @@ -454,37 +447,38 @@ class FibaroDevice(Entity): if not fibaro_device.visible: self._attr_entity_registry_visible_default = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.controller.register(self.fibaro_device.fibaro_id, self._update_callback) - def _update_callback(self): + def _update_callback(self) -> None: """Update the state.""" self.schedule_update_ha_state(True) @property - def level(self): + def level(self) -> int | None: """Get the level of Fibaro device.""" if self.fibaro_device.value.has_value: return self.fibaro_device.value.int_value() return None @property - def level2(self): + def level2(self) -> int | None: """Get the tilt level of Fibaro device.""" if self.fibaro_device.value_2.has_value: return self.fibaro_device.value_2.int_value() return None - def dont_know_message(self, action): + def dont_know_message(self, cmd: str) -> None: """Make a warning in case we don't know how to perform an action.""" _LOGGER.warning( - "Not sure how to setValue: %s (available actions: %s)", + "Not sure how to %s: %s (available actions: %s)", + cmd, str(self.ha_id), str(self.fibaro_device.actions), ) - def set_level(self, level): + def set_level(self, level: int) -> None: """Set the level of Fibaro device.""" self.action("setValue", level) if self.fibaro_device.value.has_value: @@ -492,21 +486,21 @@ class FibaroDevice(Entity): if self.fibaro_device.has_brightness: self.fibaro_device.properties["brightness"] = level - def set_level2(self, level): + def set_level2(self, level: int) -> None: """Set the level2 of Fibaro device.""" self.action("setValue2", level) if self.fibaro_device.value_2.has_value: self.fibaro_device.properties["value2"] = level - def call_turn_on(self): + def call_turn_on(self) -> None: """Turn on the Fibaro device.""" self.action("turnOn") - def call_turn_off(self): + def call_turn_off(self) -> None: """Turn off the Fibaro device.""" self.action("turnOff") - def call_set_color(self, red, green, blue, white): + def call_set_color(self, red: int, green: int, blue: int, white: int) -> None: """Set the color of Fibaro device.""" red = int(max(0, min(255, red))) green = int(max(0, min(255, green))) @@ -516,7 +510,7 @@ class FibaroDevice(Entity): self.fibaro_device.properties["color"] = color_str self.action("setColor", str(red), str(green), str(blue), str(white)) - def action(self, cmd, *args): + def action(self, cmd: str, *args: Any) -> None: """Perform an action on the Fibaro HC.""" if cmd in self.fibaro_device.actions: self.fibaro_device.execute_action(cmd, args) @@ -525,12 +519,12 @@ class FibaroDevice(Entity): self.dont_know_message(cmd) @property - def current_binary_state(self): + def current_binary_state(self) -> bool: """Return the current binary state.""" return self.fibaro_device.value.bool_value(False) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the state attributes of the device.""" attr = {"fibaro_id": self.fibaro_device.fibaro_id} diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index c0980025555..3c965c11b34 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -76,9 +76,9 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1] @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the extra state attributes of the device.""" - return super().extra_state_attributes | self._own_extra_state_attributes + return {**super().extra_state_attributes, **self._own_extra_state_attributes} def update(self) -> None: """Get the latest data and update the state.""" diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index 8c2fb502488..9003704348d 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import FibaroAuthFailed, FibaroConnectFailed, FibaroController +from . import FibaroAuthFailed, FibaroConnectFailed, init_controller from .const import CONF_IMPORT_PLUGINS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,19 +28,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -def _connect_to_fibaro(data: dict[str, Any]) -> FibaroController: - """Validate the user input allows us to connect to fibaro.""" - controller = FibaroController(data) - controller.connect_with_error_handling() - return controller - - 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 STEP_USER_DATA_SCHEMA with values provided by the user. """ - controller = await hass.async_add_executor_job(_connect_to_fibaro, data) + controller = await hass.async_add_executor_job(init_controller, data) _LOGGER.debug( "Successfully connected to fibaro home center %s with name %s", diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index 16be6e98ae1..e71ae8982e7 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from pyfibaro.fibaro_device import DeviceModel @@ -80,11 +80,11 @@ class FibaroCover(FibaroDevice, CoverEntity): def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self.set_level(kwargs.get(ATTR_POSITION)) + self.set_level(cast(int, kwargs.get(ATTR_POSITION))) def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self.set_level2(kwargs.get(ATTR_TILT_POSITION)) + self.set_level2(cast(int, kwargs.get(ATTR_TILT_POSITION))) @property def is_closed(self) -> bool | None: diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 68763228f82..39850672d06 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.6"] + "requirements": ["pyfibaro==0.7.8"] } diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 5ae300f6ec4..decb1f0a33f 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -35,13 +35,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + 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.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 6d3f1f63b84..4a4c2d05181 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import namedtuple from datetime import timedelta +from functools import cached_property import logging from typing import Any @@ -11,7 +12,6 @@ from fints.client import FinTS3PinTanClient from fints.models import SEPAAccount import voluptuous as vol -from homeassistant.backports.functools import cached_property from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index 9938f6ab096..a22991f2008 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -63,7 +63,7 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): return attr data = self.coordinator.data - attr = { + return { key: data[key] for key in ( "start_time", @@ -77,5 +77,3 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): ) if key in data } - - return attr diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 04e1e4ef5eb..22287653788 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -71,7 +71,7 @@ class ResponseSwitch(SwitchEntity): return attr data = self._state_attributes - attr = { + return { key: data[key] for key in ( "user_name", @@ -87,8 +87,6 @@ class ResponseSwitch(SwitchEntity): if key in data } - return attr - async def async_turn_on(self, **kwargs: Any) -> None: """Send Acknowledge response status.""" await self.async_set_response(True) diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index 9d177e4c2b6..0b0230f536e 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -44,9 +44,9 @@ class FliprConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except (Timeout, ConnectionError): errors["base"] = "cannot_connect" - except Exception as exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" - _LOGGER.exception(exception) + _LOGGER.exception("Unexpected exception") if not errors and not flipr_ids: # No flipr_id found. Tell the user with an error message. @@ -91,9 +91,7 @@ class FliprConfigFlow(ConfigFlow, domain=DOMAIN): # Instantiates the flipr API that does not require async since it is has no network access. client = FliprAPIRestClient(self._username, self._password) - flipr_ids = await self.hass.async_add_executor_job(client.search_flipr_ids) - - return flipr_ids + return await self.hass.async_add_executor_job(client.search_flipr_ids) async def async_step_flipr_id( self, user_input: dict[str, str] | None = None diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index c4454eba800..6c8e4fc63a9 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_files_list(folder_path: str, filter_term: str) -> list[str]: """Return the list of files, applying filter.""" query = folder_path + filter_term - files_list = glob.glob(query) - return files_list + return glob.glob(query) def get_size(files_list: list[str]) -> int: diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index d111fe03c5c..3f0b9e8f6da 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import os -from typing import cast +from typing import Any, cast import voluptuous as vol from watchdog.events import ( @@ -19,17 +19,17 @@ from watchdog.events import ( ) from watchdog.observers import Observer +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType +from .const import CONF_FOLDER, CONF_PATTERNS, DEFAULT_PATTERN, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_FOLDER = "folder" -CONF_PATTERNS = "patterns" -DEFAULT_PATTERN = "*" -DOMAIN = "folder_watcher" CONFIG_SCHEMA = vol.Schema( { @@ -51,20 +51,62 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the folder watcher.""" - conf = config[DOMAIN] - for watcher in conf: - path: str = watcher[CONF_FOLDER] - patterns: list[str] = watcher[CONF_PATTERNS] - if not hass.config.is_allowed_path(path): - _LOGGER.error("Folder %s is not valid or allowed", path) - return False - Watcher(path, patterns, hass) + if DOMAIN in config: + conf: list[dict[str, Any]] = config[DOMAIN] + for watcher in conf: + path: str = watcher[CONF_FOLDER] + if not hass.config.is_allowed_path(path): + async_create_issue( + hass, + DOMAIN, + f"import_failed_not_allowed_path_{path}", + is_fixable=False, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="import_failed_not_allowed_path", + translation_placeholders={ + "path": path, + "config_variable": "allowlist_external_dirs", + }, + ) + continue + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=watcher + ) + ) return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Folder watcher from a config entry.""" + + path: str = entry.options[CONF_FOLDER] + patterns: list[str] = entry.options[CONF_PATTERNS] + if not hass.config.is_allowed_path(path): + _LOGGER.error("Folder %s is not valid or allowed", path) + async_create_issue( + hass, + DOMAIN, + f"setup_not_allowed_path_{path}", + is_fixable=False, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="setup_not_allowed_path", + translation_placeholders={ + "path": path, + "config_variable": "allowlist_external_dirs", + }, + learn_more_url="https://www.home-assistant.io/docs/configuration/basic/#allowlist_external_dirs", + ) + return False + await hass.async_add_executor_job(Watcher, path, patterns, hass) + return True + + def create_event_handler(patterns: list[str], hass: HomeAssistant) -> EventHandler: """Return the Watchdog EventHandler object.""" diff --git a/homeassistant/components/folder_watcher/config_flow.py b/homeassistant/components/folder_watcher/config_flow.py new file mode 100644 index 00000000000..50d198df3c3 --- /dev/null +++ b/homeassistant/components/folder_watcher/config_flow.py @@ -0,0 +1,116 @@ +"""Adds config flow for Folder watcher.""" + +from __future__ import annotations + +from collections.abc import Mapping +import os +from typing import Any + +import voluptuous as vol + +from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.core import callback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) + +from .const import CONF_FOLDER, CONF_PATTERNS, DEFAULT_PATTERN, DOMAIN + + +async def validate_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Check path is a folder.""" + value: str = user_input[CONF_FOLDER] + dir_in = os.path.expanduser(str(value)) + handler.parent_handler._async_abort_entries_match({CONF_FOLDER: value}) # pylint: disable=protected-access + + if not os.path.isdir(dir_in): + raise SchemaFlowError("not_dir") + if not os.access(dir_in, os.R_OK): + raise SchemaFlowError("not_readable_dir") + if not handler.parent_handler.hass.config.is_allowed_path(value): + raise SchemaFlowError("not_allowed_dir") + + return user_input + + +async def validate_import_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Create issue on successful import.""" + async_create_issue( + handler.parent_handler.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Folder Watcher", + }, + ) + return user_input + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): SelectSelector( + SelectSelectorConfig( + options=[DEFAULT_PATTERN], + multiple=True, + custom_value=True, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } +) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_FOLDER): TextSelector(), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW = { + "user": SchemaFlowFormStep(schema=DATA_SCHEMA, validate_user_input=validate_setup), + "import": SchemaFlowFormStep( + schema=DATA_SCHEMA, validate_user_input=validate_import_setup + ), +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(schema=OPTIONS_SCHEMA), +} + + +class FolderWatcherConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Folder Watcher.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return f"Folder Watcher {options[CONF_FOLDER]}" + + @callback + def async_create_entry( + self, data: Mapping[str, Any], **kwargs: Any + ) -> ConfigFlowResult: + """Finish config flow and create a config entry.""" + self._async_abort_entries_match({CONF_FOLDER: data[CONF_FOLDER]}) + return super().async_create_entry(data, **kwargs) diff --git a/homeassistant/components/folder_watcher/const.py b/homeassistant/components/folder_watcher/const.py new file mode 100644 index 00000000000..22dae3b9164 --- /dev/null +++ b/homeassistant/components/folder_watcher/const.py @@ -0,0 +1,6 @@ +"""Constants for Folder watcher.""" + +CONF_FOLDER = "folder" +CONF_PATTERNS = "patterns" +DEFAULT_PATTERN = "*" +DOMAIN = "folder_watcher" diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 96decd0b8cf..7b471e08fcc 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,6 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/folder_watcher", "iot_class": "local_polling", "loggers": ["watchdog"], diff --git a/homeassistant/components/folder_watcher/strings.json b/homeassistant/components/folder_watcher/strings.json new file mode 100644 index 00000000000..bd1742b8ce3 --- /dev/null +++ b/homeassistant/components/folder_watcher/strings.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "not_dir": "Configured path is not a directory", + "not_readable_dir": "Configured path is not readable", + "not_allowed_dir": "Configured path is not in allowlist" + }, + "step": { + "user": { + "data": { + "folder": "Path to the watched folder", + "patterns": "Pattern(s) to monitor" + }, + "data_description": { + "folder": "Path needs to be from root, as example `/config`", + "patterns": "Example: `*.yaml` to only see yaml files" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "patterns": "[%key:component::folder_watcher::config::step::user::data::patterns%]" + }, + "data_description": { + "patterns": "[%key:component::folder_watcher::config::step::user::data_description::patterns%]" + } + } + } + }, + "issues": { + "import_failed_not_allowed_path": { + "title": "The Folder Watcher YAML configuration could not be imported", + "description": "Configuring Folder Watcher using YAML is being removed but your configuration could not be imported as the folder {path} is not in the configured allowlist.\n\nPlease add it to `{config_variable}` in config.yaml and restart Home Assistant to import it and fix this issue." + }, + "setup_not_allowed_path": { + "title": "The Folder Watcher configuration for {path} could not start", + "description": "The path {path} is not accessible or not allowed to be accessed.\n\nPlease check the path is accessible and add it to `{config_variable}` in config.yaml and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index 6f256f99854..9ddb7c4b4fc 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,7 +1,7 @@ { "domain": "foscam", "name": "Foscam", - "codeowners": ["@skgsergio", "@krmarien"], + "codeowners": ["@krmarien"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/foscam", "iot_class": "local_polling", diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 26b3e37beb3..ed2fbcf1e83 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -43,7 +43,6 @@ def is_json(json_str: str) -> bool: """Validate if a String is a JSON value or not.""" try: json.loads(json_str) - return True except (ValueError, TypeError) as err: _LOGGER.error( "Failed to parse JSON '%s', error '%s'", @@ -51,6 +50,7 @@ def is_json(json_str: str) -> bool: err, ) return False + return True async def get_api(hass: HomeAssistant, host: str) -> Freepybox: @@ -87,7 +87,7 @@ async def get_hosts_list_if_supported( ) else: - raise err + raise return supports_hosts, fbx_devices diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index ba9e2191901..bab97569eda 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -3,13 +3,20 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .common import AvmWrapper, FritzData from .const import ( DATA_FRITZ, + DEFAULT_SSL, DOMAIN, FRITZ_AUTH_EXCEPTIONS, FRITZ_EXCEPTIONS, @@ -29,6 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: port=entry.data[CONF_PORT], username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL), ) try: diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py index d56350dd1d0..cfd0e09412d 100644 --- a/homeassistant/components/fritz/button.py +++ b/homeassistant/components/fritz/button.py @@ -14,12 +14,13 @@ from homeassistant.components.button 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.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .common import AvmWrapper -from .const import DOMAIN +from .common import AvmWrapper, FritzData, FritzDevice, FritzDeviceBase, _is_tracked +from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, DATA_FRITZ, DOMAIN, MeshRoles _LOGGER = logging.getLogger(__name__) @@ -70,8 +71,28 @@ async def async_setup_entry( _LOGGER.debug("Setting up buttons") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [FritzButton(avm_wrapper, entry.title, button) for button in BUTTONS] + entities_list: list[ButtonEntity] = [ + FritzButton(avm_wrapper, entry.title, button) for button in BUTTONS + ] + + if avm_wrapper.mesh_role == MeshRoles.SLAVE: + async_add_entities(entities_list) + return + + data_fritz: FritzData = hass.data[DATA_FRITZ] + entities_list += _async_wol_buttons_list(avm_wrapper, data_fritz) + + async_add_entities(entities_list) + + @callback + def async_update_avm_device() -> None: + """Update the values of the AVM device.""" + async_add_entities(_async_wol_buttons_list(avm_wrapper, data_fritz)) + + entry.async_on_unload( + async_dispatcher_connect( + hass, avm_wrapper.signal_device_new, async_update_avm_device + ) ) @@ -101,3 +122,64 @@ class FritzButton(ButtonEntity): async def async_press(self) -> None: """Triggers Fritz!Box service.""" await self.entity_description.press_action(self.avm_wrapper) + + +@callback +def _async_wol_buttons_list( + avm_wrapper: AvmWrapper, + data_fritz: FritzData, +) -> list[FritzBoxWOLButton]: + """Add new WOL button entities from the AVM device.""" + _LOGGER.debug("Setting up %s buttons", BUTTON_TYPE_WOL) + + new_wols: list[FritzBoxWOLButton] = [] + + if avm_wrapper.unique_id not in data_fritz.wol_buttons: + data_fritz.wol_buttons[avm_wrapper.unique_id] = set() + + for mac, device in avm_wrapper.devices.items(): + if _is_tracked(mac, data_fritz.wol_buttons.values()): + _LOGGER.debug("Skipping wol button creation for device %s", device.hostname) + continue + + if device.connection_type != CONNECTION_TYPE_LAN: + _LOGGER.debug( + "Skipping wol button creation for device %s, not connected via LAN", + device.hostname, + ) + continue + + new_wols.append(FritzBoxWOLButton(avm_wrapper, device)) + data_fritz.wol_buttons[avm_wrapper.unique_id].add(mac) + + _LOGGER.debug("Creating %s wol buttons", len(new_wols)) + return new_wols + + +class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity): + """Defines a FRITZ!Box Tools Wake On LAN button.""" + + _attr_icon = "mdi:lan-pending" + _attr_entity_registry_enabled_default = False + + def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None: + """Initialize Fritz!Box WOL button.""" + super().__init__(avm_wrapper, device) + self._name = f"{self.hostname} Wake on LAN" + self._attr_unique_id = f"{self._mac}_wake_on_lan" + self._is_available = True + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + default_manufacturer="AVM", + default_model="FRITZ!Box Tracked device", + default_name=device.hostname, + via_device=( + DOMAIN, + avm_wrapper.unique_id, + ), + ) + + async def async_press(self) -> None: + """Press the button.""" + if self.mac_address: + await self._avm_wrapper.async_wake_on_lan(self.mac_address) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 5815f9abfc1..f051c824847 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -48,7 +48,7 @@ from .const import ( DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_DEVICE_NAME, DEFAULT_HOST, - DEFAULT_PORT, + DEFAULT_SSL, DEFAULT_USERNAME, DOMAIN, FRITZ_EXCEPTIONS, @@ -184,9 +184,10 @@ class FritzBoxTools( self, hass: HomeAssistant, password: str, + port: int, username: str = DEFAULT_USERNAME, host: str = DEFAULT_HOST, - port: int = DEFAULT_PORT, + use_tls: bool = DEFAULT_SSL, ) -> None: """Initialize FritzboxTools class.""" super().__init__( @@ -211,6 +212,7 @@ class FritzBoxTools( self.password = password self.port = port self.username = username + self.use_tls = use_tls self.has_call_deflections: bool = False self._model: str | None = None self._current_firmware: str | None = None @@ -230,11 +232,13 @@ class FritzBoxTools( def setup(self) -> None: """Set up FritzboxTools class.""" + self.connection = FritzConnection( address=self.host, port=self.port, user=self.username, password=self.password, + use_tls=self.use_tls, timeout=60.0, pool_maxsize=30, ) @@ -794,24 +798,26 @@ class AvmWrapper(FritzBoxTools): # pylint: disable=hass-enforce-coordinator-mod **kwargs, ) ) - return result except FritzSecurityError: _LOGGER.exception( "Authorization Error: Please check the provided credentials and" " verify that you can log into the web interface" ) + return {} except FRITZ_EXCEPTIONS: _LOGGER.exception( "Service/Action Error: cannot execute service %s with action %s", service_name, action_name, ) + return {} except FritzConnectionException: _LOGGER.exception( "Connection Error: Please check the device is properly configured" " for remote login" ) - return {} + return {} + return result async def async_get_upnp_configuration(self) -> dict[str, Any]: """Call X_AVM-DE_UPnP service.""" @@ -926,6 +932,16 @@ class AvmWrapper(FritzBoxTools): # pylint: disable=hass-enforce-coordinator-mod NewDisallow="0" if turn_on else "1", ) + async def async_wake_on_lan(self, mac_address: str) -> dict[str, Any]: + """Call X_AVM-DE_WakeOnLANByMACAddress service.""" + + return await self._async_service_call( + "Hosts", + "1", + "X_AVM-DE_WakeOnLANByMACAddress", + NewMACAddress=mac_address, + ) + @dataclass class FritzData: @@ -933,6 +949,7 @@ class FritzData: tracked: dict = field(default_factory=dict) profile_switches: dict = field(default_factory=dict) + wol_buttons: dict = field(default_factory=dict) class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]): diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index a217adf935c..fdafd486b29 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -25,14 +25,22 @@ from homeassistant.config_entries import ( OptionsFlow, OptionsFlowWithConfigEntry, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import callback from .const import ( CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_HOST, - DEFAULT_PORT, + DEFAULT_HTTP_PORT, + DEFAULT_HTTPS_PORT, + DEFAULT_SSL, DOMAIN, ERROR_AUTH_INVALID, ERROR_CANNOT_CONNECT, @@ -61,6 +69,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._entry: ConfigEntry | None = None self._name: str = "" self._password: str = "" + self._use_tls: bool = False self._port: int | None = None self._username: str = "" self._model: str = "" @@ -74,6 +83,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): port=self._port, user=self._username, password=self._password, + use_tls=self._use_tls, timeout=60.0, pool_maxsize=30, ) @@ -120,6 +130,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PASSWORD: self._password, CONF_PORT: self._port, CONF_USERNAME: self._username, + CONF_SSL: self._use_tls, }, options={ CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), @@ -127,13 +138,18 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): }, ) + def _determine_port(self, user_input: dict[str, Any]) -> int: + """Determine port from user_input.""" + if port := user_input.get(CONF_PORT): + return int(port) + return DEFAULT_HTTPS_PORT if user_input[CONF_SSL] else DEFAULT_HTTP_PORT + 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 - self._port = ssdp_location.port self._name = ( discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] @@ -178,6 +194,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] + self._use_tls = user_input[CONF_SSL] + self._port = self._determine_port(user_input) error = await self.hass.async_add_executor_job(self.fritz_tools_init) @@ -191,14 +209,22 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self, errors: dict[str, str] | None = None ) -> ConfigFlowResult: """Show the setup form to the user.""" + + advanced_data_schema = {} + if self.show_advanced_options: + advanced_data_schema = { + vol.Optional(CONF_PORT): vol.Coerce(int), + } + return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + **advanced_data_schema, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, } ), errors=errors or {}, @@ -214,6 +240,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, } ), description_placeholders={"name": self._name}, @@ -227,9 +254,11 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is None: return self._show_setup_form_init() self._host = user_input[CONF_HOST] - self._port = user_input[CONF_PORT] self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] + self._use_tls = user_input[CONF_SSL] + + self._port = self._determine_port(user_input) if not (error := await self.hass.async_add_executor_job(self.fritz_tools_init)): self._name = self._model @@ -251,6 +280,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._port = entry_data[CONF_PORT] self._username = entry_data[CONF_USERNAME] self._password = entry_data[CONF_PASSWORD] + self._use_tls = entry_data[CONF_SSL] + return await self.async_step_reauth_confirm() def _show_setup_form_reauth_confirm( @@ -295,11 +326,83 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PASSWORD: self._password, CONF_PORT: self._port, CONF_USERNAME: self._username, + CONF_SSL: self._use_tls, }, ) await self.hass.config_entries.async_reload(self._entry.entry_id) return self.async_abort(reason="reauth_successful") + async def async_step_reconfigure(self, _: Mapping[str, Any]) -> ConfigFlowResult: + """Handle reconfigure flow .""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self._entry + self._host = self._entry.data[CONF_HOST] + self._port = self._entry.data[CONF_PORT] + self._username = self._entry.data[CONF_USERNAME] + self._password = self._entry.data[CONF_PASSWORD] + self._use_tls = self._entry.data.get(CONF_SSL, DEFAULT_SSL) + + return await self.async_step_reconfigure_confirm() + + def _show_setup_form_reconfigure_confirm( + self, user_input: dict[str, Any], errors: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Show the reconfigure form to the user.""" + advanced_data_schema = {} + if self.show_advanced_options: + advanced_data_schema = { + vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int), + } + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str, + **advanced_data_schema, + vol.Required(CONF_SSL, default=user_input[CONF_SSL]): bool, + } + ), + description_placeholders={"host": self._host}, + errors=errors or {}, + ) + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfigure flow.""" + if user_input is None: + return self._show_setup_form_reconfigure_confirm( + { + CONF_HOST: self._host, + CONF_PORT: self._port, + CONF_SSL: self._use_tls, + } + ) + + self._host = user_input[CONF_HOST] + self._use_tls = user_input[CONF_SSL] + self._port = self._determine_port(user_input) + + if error := await self.hass.async_add_executor_job(self.fritz_tools_init): + return self._show_setup_form_reconfigure_confirm( + user_input={**user_input, CONF_PORT: self._port}, errors={"base": error} + ) + + assert isinstance(self._entry, ConfigEntry) + self.hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_HOST: self._host, + CONF_PASSWORD: self._password, + CONF_PORT: self._port, + CONF_USERNAME: self._username, + CONF_SSL: self._use_tls, + }, + ) + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_abort(reason="reconfigure_successful") + class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle an options flow.""" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index fb60eaef5f8..3794a83dd7f 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -46,8 +46,10 @@ DSL_CONNECTION: Literal["dsl"] = "dsl" DEFAULT_DEVICE_NAME = "Unknown device" DEFAULT_HOST = "192.168.178.1" -DEFAULT_PORT = 49000 +DEFAULT_HTTP_PORT = 49000 +DEFAULT_HTTPS_PORT = 49443 DEFAULT_USERNAME = "" +DEFAULT_SSL = False ERROR_AUTH_INVALID = "invalid_auth" ERROR_CANNOT_CONNECT = "cannot_connect" @@ -65,6 +67,8 @@ SWITCH_TYPE_PORTFORWARD = "PortForward" SWITCH_TYPE_PROFILE = "Profile" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" +BUTTON_TYPE_WOL = "WakeOnLan" + UPTIME_DEVIATION = 5 FRITZ_EXCEPTIONS = ( @@ -79,3 +83,5 @@ FRITZ_EXCEPTIONS = ( FRITZ_AUTH_EXCEPTIONS = (FritzAuthorizationError, FritzSecurityError) WIFI_STANDARD = {1: "2.4Ghz", 2: "5Ghz", 3: "5Ghz", 4: "Guest"} + +CONNECTION_TYPE_LAN = "LAN" diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index 3136f03f95b..c4725b99e43 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -21,7 +21,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - diag_data = { + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": { "model": avm_wrapper.model, @@ -51,5 +51,3 @@ async def async_get_config_entry_diagnostics( "wan_link_properties": await avm_wrapper.async_get_wan_link_properties(), }, } - - return diag_data diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 5eed2f59fc4..a96c3b8ac28 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -18,6 +18,19 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "reconfigure_confirm": { + "title": "Updating FRITZ!Box Tools - configuration", + "description": "Update FRITZ!Box Tools configuration for: {host}.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "[%key:common::config_flow::data::ssl%]" + }, + "data_description": { + "host": "The hostname or IP address of your FRITZ!Box router.", + "port": "Leave it empty to use the default port." + } + }, "user": { "title": "[%key:component::fritz::config::step::confirm::title%]", "description": "Set up FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", @@ -25,10 +38,12 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]" }, "data_description": { - "host": "The hostname or IP address of your FRITZ!Box router." + "host": "The hostname or IP address of your FRITZ!Box router.", + "port": "Leave it empty to use the default port." } } }, @@ -36,7 +51,8 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "ignore_ip6_link_local": "IPv6 link local address is not supported.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7f4006768c4..904a86d21ae 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -51,12 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: has_templates = await hass.async_add_executor_job(fritz.has_templates) LOGGER.debug("enable smarthome templates: %s", has_templates) - coordinator = FritzboxDataUpdateCoordinator(hass, entry, has_templates) - - await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator - def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if ( @@ -79,6 +73,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, _update_unique_id) + coordinator = FritzboxDataUpdateCoordinator(hass, entry.entry_id, has_templates) + await coordinator.async_setup() + hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def logout_fritzbox(event: Event) -> None: diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 17accf35819..de9ec200e3e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -29,6 +29,7 @@ from .const import ( ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, + LOGGER, ) from .model import ClimateExtraAttributes @@ -129,6 +130,11 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" + if self.hvac_mode == hvac_mode: + LOGGER.debug( + "%s is already in requested hvac mode %s", self.name, hvac_mode + ) + return if hvac_mode == HVACMode.OFF: await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) else: diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index e32f27969a1..62f189b542f 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -82,13 +82,13 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): fritzbox.login() fritzbox.get_device_elements() fritzbox.logout() - return RESULT_SUCCESS except LoginError: return RESULT_INVALID_AUTH except HTTPError: return RESULT_NOT_SUPPORTED except OSError: return RESULT_NO_DEVICES_FOUND + return RESULT_SUCCESS async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -221,3 +221,44 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={"name": self._name}, errors=errors, ) + + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + self._entry = entry + self._name = self._entry.data[CONF_HOST] + self._host = self._entry.data[CONF_HOST] + self._username = self._entry.data[CONF_USERNAME] + self._password = self._entry.data[CONF_PASSWORD] + + 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 = {} + + if user_input is not None: + self._host = user_input[CONF_HOST] + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result == RESULT_SUCCESS: + await self._update_entry() + return self.async_abort(reason="reconfigure_successful") + errors["base"] = result + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self._host): str, + } + ), + description_placeholders={"name": self._name}, + errors=errors, + ) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index c58665f2b5d..54af8fbdacd 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -12,6 +12,7 @@ from requests.exceptions import ConnectionError as RequestConnectionError, HTTPE from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_CONNECTIONS, DOMAIN, LOGGER @@ -28,34 +29,62 @@ class FritzboxCoordinatorData: class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]): """Fritzbox Smarthome device data update coordinator.""" + config_entry: ConfigEntry configuration_url: str - def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, has_templates: bool - ) -> None: + def __init__(self, hass: HomeAssistant, name: str, has_templates: bool) -> None: """Initialize the Fritzbox Smarthome device coordinator.""" - self.entry = entry - self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] + super().__init__( + hass, + LOGGER, + name=name, + update_interval=timedelta(seconds=30), + ) + + self.fritz: Fritzhome = hass.data[DOMAIN][self.config_entry.entry_id][ + CONF_CONNECTIONS + ] self.configuration_url = self.fritz.get_prefixed_host() self.has_templates = has_templates self.new_devices: set[str] = set() self.new_templates: set[str] = set() - super().__init__( - hass, - LOGGER, - name=entry.entry_id, - update_interval=timedelta(seconds=30), + self.data = FritzboxCoordinatorData({}, {}) + + async def async_setup(self) -> None: + """Set up the coordinator.""" + await self.async_config_entry_first_refresh() + self.cleanup_removed_devices( + list(self.data.devices) + list(self.data.templates) ) - self.data = FritzboxCoordinatorData({}, {}) + def cleanup_removed_devices(self, available_ains: list[str]) -> None: + """Cleanup entity and device registry from removed devices.""" + entity_reg = er.async_get(self.hass) + for entity in er.async_entries_for_config_entry( + entity_reg, self.config_entry.entry_id + ): + if entity.unique_id.split("_")[0] not in available_ains: + LOGGER.debug("Removing obsolete entity entry %s", entity.entity_id) + entity_reg.async_remove(entity.entity_id) + + device_reg = dr.async_get(self.hass) + identifiers = {(DOMAIN, ain) for ain in available_ains} + for device in dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ): + if not set(device.identifiers) & identifiers: + LOGGER.debug("Removing obsolete device entry %s", device.name) + device_reg.async_update_device( + device.id, remove_config_entry_id=self.config_entry.entry_id + ) def _update_fritz_devices(self) -> FritzboxCoordinatorData: """Update all fritzbox device data.""" try: - self.fritz.update_devices() + self.fritz.update_devices(ignore_removed=False) if self.has_templates: - self.fritz.update_templates() + self.fritz.update_templates(ignore_removed=False) except RequestConnectionError as ex: raise UpdateFailed from ex except HTTPError: @@ -64,9 +93,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat self.fritz.login() except LoginError as ex: raise ConfigEntryAuthFailed from ex - self.fritz.update_devices() + self.fritz.update_devices(ignore_removed=False) if self.has_templates: - self.fritz.update_templates() + self.fritz.update_templates(ignore_removed=False) devices = self.fritz.get_devices() device_data = {} @@ -99,4 +128,14 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat async def _async_update_data(self) -> FritzboxCoordinatorData: """Fetch all device data.""" - return await self.hass.async_add_executor_job(self._update_fritz_devices) + new_data = await self.hass.async_add_executor_job(self._update_fritz_devices) + + if ( + self.data.devices.keys() - new_data.devices.keys() + or self.data.templates.keys() - new_data.templates.keys() + ): + self.cleanup_removed_devices( + list(new_data.devices) + list(new_data.templates) + ) + + return new_data diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 5d41f8c12dc..de2e9e0200a 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["pyfritzhome"], "quality_scale": "gold", - "requirements": ["pyfritzhome==0.6.10"], + "requirements": ["pyfritzhome==0.6.11"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index f4d2fe3670e..755cc97d7d8 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -26,6 +26,15 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reconfigure_confirm": { + "description": "Update your configuration information for {name}.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of your FRITZ!Box router." + } } }, "abort": { @@ -34,7 +43,8 @@ "ignore_ip6_link_local": "IPv6 link local address is not supported.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index ac0d3ea3337..019326d840c 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -109,9 +109,6 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN): address=self._host, user=self._username, password=self._password ) info = fritz_connection.updatecheck - self._serial_number = info[FRITZ_ATTR_SERIAL_NUMBER] - - return ConnectResult.SUCCESS except RequestsConnectionError: return ConnectResult.NO_DEVIES_FOUND except FritzSecurityError: @@ -119,6 +116,9 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN): except FritzConnectionException: return ConnectResult.INVALID_AUTH + self._serial_number = info[FRITZ_ATTR_SERIAL_NUMBER] + return ConnectResult.SUCCESS + async def _get_name_of_phonebook(self, phonebook_id: int) -> str: """Return name of phonebook for given phonebook_id.""" phonebook_info = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index d0a20b25bee..1ecd74a6e09 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -152,9 +152,9 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): data = await self.solar_net.fronius.current_inverter_data( self.inverter_info.solar_net_id ) - except BadStatusError as err: + except BadStatusError: if silent_retry == (self.SILENT_RETRIES - 1): - raise err + raise continue break # wrap a single devices data in a dict with solar_net_id key for diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d711314cabb..6abe8df1d7c 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==20240404.2"] + "requirements": ["home-assistant-frontend==20240501.0"] } diff --git a/homeassistant/components/frontier_silicon/browse_media.py b/homeassistant/components/frontier_silicon/browse_media.py index da5169b8e7c..0b51cb767c7 100644 --- a/homeassistant/components/frontier_silicon/browse_media.py +++ b/homeassistant/components/frontier_silicon/browse_media.py @@ -94,7 +94,7 @@ async def browse_top_level(current_mode, afsapi: AFSAPI): for top_level_media_content_id, name in TOP_LEVEL_DIRECTORIES.items() ] - library_info = BrowseMedia( + return BrowseMedia( media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type=MediaType.CHANNELS, @@ -105,8 +105,6 @@ async def browse_top_level(current_mode, afsapi: AFSAPI): children_media_class=MediaClass.DIRECTORY, ) - return library_info - async def browse_node( afsapi: AFSAPI, diff --git a/homeassistant/components/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py index a9c87cd9d4a..cf775b15138 100644 --- a/homeassistant/components/frontier_silicon/config_flow.py +++ b/homeassistant/components/frontier_silicon/config_flow.py @@ -74,8 +74,8 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN): self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url) except FSConnectionError: errors["base"] = "cannot_connect" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return await self._async_step_device_config_if_needed() @@ -206,8 +206,8 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidPinException: errors["base"] = "invalid_auth" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: if self._reauth_entry: diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index febd5b94469..205dd97a42f 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -2,15 +2,23 @@ from __future__ import annotations +from datetime import datetime import logging +from typing import Any +from zoneinfo import ZoneInfo from fyta_cli.fyta_connector import FytaConnector from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import CONF_EXPIRATION, DOMAIN from .coordinator import FytaCoordinator _LOGGER = logging.getLogger(__name__) @@ -22,11 +30,16 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Fyta integration.""" + tz: str = hass.config.time_zone username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] + access_token: str = entry.data[CONF_ACCESS_TOKEN] + expiration: datetime = datetime.fromisoformat( + entry.data[CONF_EXPIRATION] + ).astimezone(ZoneInfo(tz)) - fyta = FytaConnector(username, password) + fyta = FytaConnector(username, password, access_token, expiration, tz) coordinator = FytaCoordinator(hass, fyta) @@ -47,3 +60,36 @@ 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: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + new = {**config_entry.data} + if config_entry.minor_version < 2: + fyta = FytaConnector( + config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + ) + credentials: dict[str, Any] = await fyta.login() + await fyta.client.close() + + new[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] + new[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + + hass.config_entries.async_update_entry( + config_entry, data=new, minor_version=2, version=1 + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index 67e46f8125e..3d83c099ac3 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -13,10 +14,10 @@ from fyta_cli.fyta_exceptions 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 .const import DOMAIN +from .const import CONF_EXPIRATION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -26,40 +27,85 @@ DATA_SCHEMA = vol.Schema( ) -class FytaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FytaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fyta.""" VERSION = 1 + MINOR_VERSION = 2 + + def __init__(self) -> None: + """Initialize FytaConfigFlow.""" + self.credentials: dict[str, Any] = {} + self._entry: ConfigEntry | None = None + + async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: + """Reusable Auth Helper.""" + fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + + try: + self.credentials = await fyta.login() + except FytaConnectionError: + return {"base": "cannot_connect"} + except FytaAuthentificationError: + return {"base": "invalid_auth"} + except FytaPasswordError: + return {"base": "invalid_auth", CONF_PASSWORD: "password_error"} + except Exception as e: # pylint: disable=broad-except + _LOGGER.error(e) + return {"base": "unknown"} + finally: + await fyta.client.close() + + self.credentials[CONF_EXPIRATION] = self.credentials[ + CONF_EXPIRATION + ].isoformat() + + return {} async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> 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: + if not (errors := await self.async_auth(user_input)): + user_input |= self.credentials 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 ) + + 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 + ) -> ConfigFlowResult: + """Handle reauthorization flow.""" + errors = {} + assert self._entry is not None + + if user_input and not (errors := await self.async_auth(user_input)): + user_input |= self.credentials + return self.async_update_reload_and_abort( + self._entry, data={**self._entry.data, **user_input} + ) + + data_schema = self.add_suggested_values_to_schema( + DATA_SCHEMA, + {CONF_USERNAME: self._entry.data[CONF_USERNAME], **(user_input or {})}, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=data_schema, + errors=errors, + ) diff --git a/homeassistant/components/fyta/const.py b/homeassistant/components/fyta/const.py index f99735dc6fa..bf4636a713a 100644 --- a/homeassistant/components/fyta/const.py +++ b/homeassistant/components/fyta/const.py @@ -1,3 +1,4 @@ """Const for fyta integration.""" DOMAIN = "fyta" +CONF_EXPIRATION = "expiration" diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index c132ee75e72..021bddf2cf8 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -12,10 +12,13 @@ from fyta_cli.fyta_exceptions import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import CONF_EXPIRATION + _LOGGER = logging.getLogger(__name__) @@ -39,17 +42,33 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): ) -> dict[int, dict[str, Any]]: """Fetch data from API endpoint.""" - if self.fyta.expiration is None or self.fyta.expiration < datetime.now(): + if ( + self.fyta.expiration is None + or self.fyta.expiration.timestamp() < datetime.now().timestamp() + ): await self.renew_authentication() return await self.fyta.update_all_plants() - async def renew_authentication(self) -> None: + async def renew_authentication(self) -> bool: """Renew access token for FYTA API.""" + credentials: dict[str, Any] = {} try: - await self.fyta.login() + credentials = await self.fyta.login() except FytaConnectionError as ex: raise ConfigEntryNotReady from ex except (FytaAuthentificationError, FytaPasswordError) as ex: - raise ConfigEntryError from ex + raise ConfigEntryAuthFailed from ex + + new_config_entry = {**self.config_entry.data} + new_config_entry[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] + new_config_entry[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + + self.hass.config_entries.async_update_entry( + self.config_entry, data=new_config_entry + ) + + _LOGGER.debug("Credentials successfully updated") + + return True diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index 55255777994..020ab330152 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/fyta", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["fyta_cli==0.3.5"] + "requirements": ["fyta_cli==0.4.1"] } diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 2b9e8e3de07..c3e90cef28e 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Final -from fyta_cli.fyta_connector import PLANT_STATUS +from fyta_cli.fyta_connector import PLANT_MEASUREMENT_STATUS, PLANT_STATUS from homeassistant.components.sensor import ( SensorDeviceClass, @@ -34,7 +34,15 @@ class FytaSensorEntityDescription(SensorEntityDescription): ) -PLANT_STATUS_LIST: list[str] = ["too_low", "low", "perfect", "high", "too_high"] +PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"] +PLANT_MEASUREMENT_STATUS_LIST: list[str] = [ + "no_data", + "too_low", + "low", + "perfect", + "high", + "too_high", +] SENSORS: Final[list[FytaSensorEntityDescription]] = [ FytaSensorEntityDescription( @@ -52,29 +60,29 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ key="temperature_status", translation_key="temperature_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="light_status", translation_key="light_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="moisture_status", translation_key="moisture_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="salinity_status", translation_key="salinity_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="temperature", diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index 6d4fe68a86c..bacd24555b0 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -8,8 +8,19 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Update your credentials for FYTA API", + "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_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", @@ -25,6 +36,16 @@ "plant_status": { "name": "Plant state", "state": { + "deleted": "Deleted", + "doing_great": "Doing great", + "need_attention": "Needs attention", + "no_sensor": "No sensor" + } + }, + "temperature_status": { + "name": "Temperature state", + "state": { + "no_data": "No data", "too_low": "Too low", "low": "Low", "perfect": "Perfect", @@ -32,44 +53,37 @@ "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%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_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%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_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%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "light": { diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 8fdc0143700..af33ae3b36f 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -291,7 +291,7 @@ async def async_test_stream( return {CONF_STREAM_SOURCE: "stream_no_route_to_host"} if err.errno == EIO: # input/output error return {CONF_STREAM_SOURCE: "stream_io_error"} - raise err + raise return {} diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 02641acccae..32ad34773bd 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -34,6 +34,7 @@ from homeassistant.const import ( from homeassistant.core import ( DOMAIN as HA_DOMAIN, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -41,7 +42,7 @@ from homeassistant.core import ( from homeassistant.helpers import condition from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - async_track_state_change, + async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity @@ -179,11 +180,15 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await super().async_added_to_hass() # Add listener - async_track_state_change( - self.hass, self._sensor_entity_id, self._async_sensor_changed + self.async_on_remove( + async_track_state_change_event( + self.hass, self._sensor_entity_id, self._async_sensor_changed_event + ) ) - async_track_state_change( - self.hass, self._switch_entity_id, self._async_switch_changed + self.async_on_remove( + async_track_state_change_event( + self.hass, self._switch_entity_id, self._async_switch_changed_event + ) ) if self._keep_alive: @@ -343,6 +348,15 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): # Get default humidity from super class return super().max_humidity + async def _async_sensor_changed_event( + self, event: Event[EventStateChangedData] + ) -> None: + """Handle ambient humidity changes.""" + data = event.data + await self._async_sensor_changed( + data["entity_id"], data["old_state"], data["new_state"] + ) + async def _async_sensor_changed( self, entity_id: str, old_state: State | None, new_state: State | None ) -> None: @@ -374,6 +388,14 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): _LOGGER.warning("Sensor is stalled, call the emergency stop") await self._async_update_humidity("Stalled") + @callback + def _async_switch_changed_event(self, event: Event[EventStateChangedData]) -> None: + """Handle humidifier switch state changes.""" + data = event.data + self._async_switch_changed( + data["entity_id"], data["old_state"], data["new_state"] + ) + @callback def _async_switch_changed( self, entity_id: str, old_state: State | None, new_state: State | None diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 42fd2ef6f41..4c660bd03e9 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -45,6 +45,7 @@ from homeassistant.core import ( DOMAIN as HA_DOMAIN, CoreState, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -54,7 +55,6 @@ from homeassistant.helpers import condition 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, async_track_time_interval, ) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 6c3b5223ef9..f17560ebc62 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -75,9 +75,9 @@ class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterEntity): return list(HA_OPMODE_TO_GH) @property - def current_operation(self) -> str: + def current_operation(self) -> str | None: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._zone.data["mode"]] # type: ignore[return-value] + return GH_STATE_TO_HA[self._zone.data["mode"]] async def async_set_operation_mode(self, operation_mode: str) -> None: """Set a new operation mode for this boiler.""" diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index d81fab65dc9..48e2f35ccc1 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -17,12 +18,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = "distance" diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index fb6140f707c..96244e08d1b 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZON from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -18,11 +19,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain -from homeassistant.helpers.event import ( - EventStateChangedData, - TrackStates, - async_track_state_change_filtered, -) +from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 178c72d2071..b72ad4bc04c 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -38,9 +38,10 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) for identifier in device.identifiers - if identifier[0] == GF_DOMAIN } if dev_ids: diff --git a/homeassistant/components/gios/diagnostics.py b/homeassistant/components/gios/diagnostics.py index 1cdd9299a1c..0bdd8f3a7ef 100644 --- a/homeassistant/components/gios/diagnostics.py +++ b/homeassistant/components/gios/diagnostics.py @@ -18,9 +18,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: GiosDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "config_entry": config_entry.as_dict(), "coordinator_data": asdict(coordinator.data), } - - return diagnostics_data diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 2e33bc6741e..b509806d07f 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["dacite", "gios"], "quality_scale": "platinum", - "requirements": ["gios==3.2.2"] + "requirements": ["gios==4.0.0"] } diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 25d8782618f..1f0fbc71efe 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -148,7 +148,9 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="could_not_register") if self.login_task is None: - self.login_task = self.hass.async_create_task(_wait_for_login()) + self.login_task = self.hass.async_create_task( + _wait_for_login(), eager_start=False + ) if self.login_task.done(): if self.login_task.exception(): diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 1bc6c96c4b8..cae2e7faca9 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/github", "iot_class": "cloud_polling", "loggers": ["aiogithubapi"], - "requirements": ["aiogithubapi==22.10.1"] + "requirements": ["aiogithubapi==23.11.0"] } diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 9a1b281eec2..4e5bdcc1543 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -1,5 +1,6 @@ """Coordinator for Glances integration.""" +from datetime import datetime, timedelta import logging from typing import Any @@ -10,6 +11,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import parse_duration, utcnow from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -42,4 +44,16 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from err except exceptions.GlancesApiError as err: raise UpdateFailed from err + # Update computed values + uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None + up_duration: timedelta | None = None + if up_duration := parse_duration(data.get("uptime")): + # Update uptime if previous value is None or previous uptime is bigger than + # new uptime (i.e. server restarted) + if ( + self.data is None + or self.data["computed"]["uptime_duration"] > up_duration + ): + uptime = utcnow() - up_duration + data["computed"] = {"uptime_duration": up_duration, "uptime": uptime} return data or {} diff --git a/homeassistant/components/glances/icons.json b/homeassistant/components/glances/icons.json index 6a8c2fa728c..f6a1dcfea39 100644 --- a/homeassistant/components/glances/icons.json +++ b/homeassistant/components/glances/icons.json @@ -10,6 +10,12 @@ "disk_free": { "default": "mdi:harddisk" }, + "diskio_read": { + "default": "mdi:harddisk" + }, + "diskio_write": { + "default": "mdi:harddisk" + }, "memory_usage": { "default": "mdi:memory" }, @@ -45,6 +51,21 @@ }, "raid_used": { "default": "mdi:harddisk" + }, + "uptime": { + "default": "mdi:clock-time-eight-outline" + }, + "gpu_processor_usage": { + "default": "mdi:expansion-card" + }, + "gpu_memory_usage": { + "default": "mdi:memory" + }, + "network_rx": { + "default": "mdi:transmission-tower" + }, + "network_tx": { + "default": "mdi:transmission-tower" } } } diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index d022995b786..2fb5cf16996 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/glances", "iot_class": "local_polling", "loggers": ["glances_api"], - "requirements": ["glances-api==0.5.0"] + "requirements": ["glances-api==0.6.0"] } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7db06a08496..c5706757725 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -3,23 +3,22 @@ from __future__ import annotations from dataclasses import dataclass -from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, - StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, REVOLUTIONS_PER_MINUTE, + UnitOfDataRate, UnitOfInformation, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +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 @@ -59,6 +58,24 @@ SENSOR_TYPES = { device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, ), + ("diskio", "read"): GlancesSensorEntityDescription( + key="read", + type="diskio", + translation_key="diskio_read", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("diskio", "write"): GlancesSensorEntityDescription( + key="write", + type="diskio", + translation_key="diskio_write", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), ("mem", "memory_use_percent"): GlancesSensorEntityDescription( key="memory_use_percent", type="mem", @@ -212,6 +229,60 @@ SENSOR_TYPES = { translation_key="raid_used", state_class=SensorStateClass.MEASUREMENT, ), + ("computed", "uptime"): GlancesSensorEntityDescription( + key="uptime", + type="computed", + translation_key="uptime", + device_class=SensorDeviceClass.TIMESTAMP, + ), + ("gpu", "mem"): GlancesSensorEntityDescription( + key="mem", + type="gpu", + translation_key="gpu_memory_usage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("gpu", "proc"): GlancesSensorEntityDescription( + key="proc", + type="gpu", + translation_key="gpu_processor_usage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + ("gpu", "temperature"): GlancesSensorEntityDescription( + key="temperature", + type="gpu", + translation_key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("gpu", "fan_speed"): GlancesSensorEntityDescription( + key="fan_speed", + type="gpu", + translation_key="fan_speed", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("network", "rx"): GlancesSensorEntityDescription( + key="rx", + type="network", + translation_key="network_rx", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("network", "tx"): GlancesSensorEntityDescription( + key="tx", + type="network", + translation_key="network_tx", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -226,7 +297,7 @@ async def async_setup_entry( entities: list[GlancesSensor] = [] for sensor_type, sensors in coordinator.data.items(): - if sensor_type in ["fs", "sensors", "raid"]: + if sensor_type in ["fs", "diskio", "sensors", "raid", "gpu", "network"]: entities.extend( GlancesSensor( coordinator, @@ -276,6 +347,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit self._attr_unique_id = ( f"{coordinator.config_entry.entry_id}-{sensor_label}-{description.key}" ) + self._update_native_value() @property def available(self) -> bool: @@ -289,13 +361,18 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit ) return False - @property - def native_value(self) -> StateType: - """Return the state of the resources.""" - value = self.coordinator.data[self.entity_description.type] + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_native_value() + super()._handle_coordinator_update() - if isinstance(value.get(self._sensor_label), dict): - return cast( - StateType, value[self._sensor_label][self.entity_description.key] - ) - return cast(StateType, value[self.entity_description.key]) + def _update_native_value(self) -> None: + """Update sensor native value from coordinator data.""" + data = self.coordinator.data[self.entity_description.type] + if dict_val := data.get(self._sensor_label): + self._attr_native_value = dict_val.get(self.entity_description.key) + elif self.entity_description.key in data: + self._attr_native_value = data.get(self.entity_description.key) + else: + self._attr_native_value = None diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index b0b535ce8ed..11735601ce9 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -41,6 +41,12 @@ "disk_free": { "name": "{sensor_label} disk free" }, + "diskio_read": { + "name": "{sensor_label} disk read" + }, + "diskio_write": { + "name": "{sensor_label} disk write" + }, "memory_usage": { "name": "Memory usage" }, @@ -100,6 +106,21 @@ }, "raid_used": { "name": "{sensor_label} used" + }, + "uptime": { + "name": "Uptime" + }, + "gpu_memory_usage": { + "name": "{sensor_label} memory usage" + }, + "gpu_processor_usage": { + "name": "{sensor_label} processor usage" + }, + "network_rx": { + "name": "{sensor_label} RX" + }, + "network_tx": { + "name": "{sensor_label} TX" } } }, diff --git a/homeassistant/components/goodwe/diagnostics.py b/homeassistant/components/goodwe/diagnostics.py index 600f02b9b7e..66806d31589 100644 --- a/homeassistant/components/goodwe/diagnostics.py +++ b/homeassistant/components/goodwe/diagnostics.py @@ -18,7 +18,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" inverter: Inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] - diagnostics_data = { + return { "config_entry": config_entry.as_dict(), "inverter": { "model_name": inverter.model_name, @@ -32,5 +32,3 @@ async def async_get_config_entry_diagnostics( "arm_svn_version": inverter.arm_svn_version, }, } - - return diagnostics_data diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 03575f9f4e2..6f1bdd2b449 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/goodwe", "iot_class": "local_polling", "loggers": ["goodwe"], - "requirements": ["goodwe==0.2.32"] + "requirements": ["goodwe==0.3.2"] } diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 26a341bd7b6..7fbe4bab5a9 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -4,12 +4,19 @@ from __future__ import annotations from collections import deque import logging -from typing import Any +from typing import TYPE_CHECKING, Any from uuid import uuid4 -from homeassistant.const import MATCH_ALL -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback -from homeassistant.helpers.event import async_call_later, async_track_state_change +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HassJob, + HomeAssistant, + callback, +) +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.significant_change import create_checker from .const import DOMAIN @@ -31,7 +38,9 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): +def async_enable_report_state( + hass: HomeAssistant, google_config: AbstractConfig +) -> CALLBACK_TYPE: """Enable state and notification reporting.""" checker = None unsub_pending: CALLBACK_TYPE | None = None @@ -60,33 +69,36 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig report_states_job = HassJob(report_states) - async def async_entity_state_listener( - changed_entity: str, old_state: State | None, new_state: State | None - ) -> None: - nonlocal unsub_pending, checker - - if not hass.is_running: - return - - if not new_state: - return - - if not google_config.should_expose(new_state): - return - - if not ( - entity := async_get_google_entity_if_supported_cached( + @callback + def _async_entity_state_filter(data: EventStateChangedData) -> bool: + return bool( + hass.is_running + and (new_state := data["new_state"]) + and google_config.should_expose(new_state) + and async_get_google_entity_if_supported_cached( hass, google_config, new_state ) - ): - return + ) + + async def _async_entity_state_listener(event: Event[EventStateChangedData]) -> None: + """Handle state changes.""" + nonlocal unsub_pending, checker + data = event.data + new_state = data["new_state"] + if TYPE_CHECKING: + assert new_state is not None # verified in filter + entity = async_get_google_entity_if_supported_cached( + hass, google_config, new_state + ) + if TYPE_CHECKING: + assert entity is not None # verified in filter # We only trigger notifications on changes in the state value, not attributes. # This is mainly designed for our event entity types # We need to synchronize notifications using a `SYNC` response, # together with other state changes. if ( - old_state + (old_state := data["old_state"]) and old_state.state != new_state.state and (notifications := entity.notifications_serialize()) is not None ): @@ -106,6 +118,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig result, ) + changed_entity = data["entity_id"] try: entity_data = entity.query_serialize() except SmartHomeError as err: @@ -173,7 +186,11 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig await google_config.async_report_state_all({"devices": {"states": entities}}) - unsub = async_track_state_change(hass, MATCH_ALL, async_entity_state_listener) + unsub = hass.bus.async_listen( + EVENT_STATE_CHANGED, + _async_entity_state_listener, + event_filter=_async_entity_state_filter, + ) unsub = async_call_later( hass, INITIAL_REPORT_DELAY, HassJob(initial_report, cancel_on_shutdown=True) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index df55fc0d7c8..a03d7c397cc 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -139,9 +139,7 @@ async def async_devices_sync( await data.config.async_connect_agent_user(agent_user_id) devices = await async_devices_sync_response(hass, data.config, agent_user_id) - response = create_sync_response(agent_user_id, devices) - - return response + return create_sync_response(agent_user_id, devices) @HANDLERS.register("action.devices.QUERY") @@ -264,7 +262,7 @@ async def handle_devices_execute( ), EXECUTE_LIMIT, ) - for entity_id, result in zip(executions, execute_results): + for entity_id, result in zip(executions, execute_results, strict=False): if result is not None: results[entity_id] = result except TimeoutError: diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index dd1e0cb3409..3efeabfa778 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1927,9 +1927,7 @@ class ModesTrait(_Trait): # Shortcut since all domains are currently unique break - payload = {"availableModes": modes} - - return payload + return {"availableModes": modes} def query_attributes(self): """Return current modes.""" @@ -2104,9 +2102,7 @@ class InputSelectorTrait(_Trait): for source in sourcelist ] - payload = {"availableInputs": inputs, "orderedInputs": True} - - return payload + return {"availableInputs": inputs, "orderedInputs": True} def query_attributes(self): """Return current modes.""" diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 0c4d081961f..ccd0fe765ac 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -70,7 +70,7 @@ async def async_send_text_commands( except aiohttp.ClientResponseError as err: if 400 <= err.status < 500: entry.async_start_reauth(hass) - raise err + raise credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 6f4751850aa..cd5c53b5fd7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -295,7 +295,7 @@ class GoogleCloudTTSProvider(Provider): except TimeoutError as ex: _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Error occurred during Google Cloud TTS call: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error occurred during Google Cloud TTS call") return None, None diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 74e2a297ff4..f289fae2456 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -11,7 +11,7 @@ from google.cloud.pubsub_v1 import PublisherClient import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.typing import ConfigType @@ -59,9 +59,9 @@ def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: encoder = DateTimeJSONEncoder() - def send_to_pubsub(event: Event): + def send_to_pubsub(event: Event[EventStateChangedData]): """Send states to Pub/Sub.""" - state = event.data.get("new_state") + state = event.data["new_state"] if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index bf7cf7c40df..f346f913e0c 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -96,9 +96,9 @@ async def async_setup_service(hass: HomeAssistant) -> None: service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) try: sheet = service.open_by_key(entry.unique_id) - except RefreshError as ex: + except RefreshError: entry.async_start_reauth(hass) - raise ex + raise except APIError as ex: raise HomeAssistantError("Failed to write data") from ex diff --git a/homeassistant/components/google_tasks/__init__.py b/homeassistant/components/google_tasks/__init__.py index b62bd0fe5a2..29a1b20f2bc 100644 --- a/homeassistant/components/google_tasks/__init__.py +++ b/homeassistant/components/google_tasks/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations -from aiohttp import ClientError +from aiohttp import ClientError, 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 config_entry_oauth2_flow from . import api @@ -18,8 +18,6 @@ PLATFORMS: list[Platform] = [Platform.TODO] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Tasks from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -29,10 +27,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: auth = api.AsyncConfigEntryAuth(hass, session) try: await auth.async_get_access_token() + except ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from err + raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id] = auth + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py index 14cd89fcec7..a9ef5c7ff23 100644 --- a/homeassistant/components/google_tasks/config_flow.py +++ b/homeassistant/components/google_tasks/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Google Tasks.""" +from collections.abc import Mapping import logging from typing import Any @@ -8,7 +9,7 @@ from googleapiclient.discovery import build from googleapiclient.errors import HttpError from googleapiclient.http import HttpRequest -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow @@ -22,6 +23,8 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN + reauth_entry: ConfigEntry | None = None + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -39,11 +42,21 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow.""" + credentials = Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]) try: + user_resource = build( + "oauth2", + "v2", + credentials=credentials, + ) + user_resource_cmd: HttpRequest = user_resource.userinfo().get() + user_resource_info = await self.hass.async_add_executor_job( + user_resource_cmd.execute + ) resource = build( "tasks", "v1", - credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]), + credentials=credentials, ) cmd: HttpRequest = resource.tasklists().list() await self.hass.async_add_executor_job(cmd.execute) @@ -53,7 +66,35 @@ class OAuth2FlowHandler( reason="access_not_configured", description_placeholders={"message": error}, ) - except Exception as ex: # pylint: disable=broad-except - self.logger.exception("Unknown error occurred: %s", ex) + except Exception: # pylint: disable=broad-except + self.logger.exception("Unknown error occurred") return self.async_abort(reason="unknown") - return self.async_create_entry(title=self.flow_impl.name, data=data) + user_id = user_resource_info["id"] + if not self.reauth_entry: + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_resource_info["name"], data=data) + + if self.reauth_entry.unique_id == user_id or not self.reauth_entry.unique_id: + return self.async_update_reload_and_abort( + self.reauth_entry, unique_id=user_id, data=data + ) + + return self.async_abort(reason="wrong_account") + + 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/google_tasks/const.py b/homeassistant/components/google_tasks/const.py index 87253486127..0cb04bf1d4e 100644 --- a/homeassistant/components/google_tasks/const.py +++ b/homeassistant/components/google_tasks/const.py @@ -6,7 +6,10 @@ DOMAIN = "google_tasks" OAUTH2_AUTHORIZE = "https://accounts.google.com/o/oauth2/v2/auth" OAUTH2_TOKEN = "https://oauth2.googleapis.com/token" -OAUTH2_SCOPES = ["https://www.googleapis.com/auth/tasks"] +OAUTH2_SCOPES = [ + "https://www.googleapis.com/auth/tasks", + "https://www.googleapis.com/auth/userinfo.profile", +] class TaskStatus(StrEnum): diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index 2cf15f0d93d..4479b34935e 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -18,6 +18,7 @@ "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "access_not_configured": "Unable to access the Google API:\n\n{message}", "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "Wrong account: Please authenticate with the right account.", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index df7130e09e0..c34713caef7 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -162,8 +162,8 @@ class GoogleProvider(Provider): try: tts.write_to_fp(mp3_data) - except gTTSError as exc: - _LOGGER.exception("Error during processing of TTS request %s", exc) + except gTTSError: + _LOGGER.exception("Error during processing of TTS request") return None, None return "mp3", mp3_data.getvalue() diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 64feedc44c1..98b802f8233 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -90,5 +90,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.31.0"] + "requirements": ["govee-ble==0.31.2"] } diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/govee_ble/strings.json +++ b/homeassistant/components/govee_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 4a28606662f..b1c7ad9091f 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -48,9 +48,8 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) for identifier in device.identifiers - if identifier[0] == GPL_DOMAIN } if not dev_ids: return diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 1290fc9459a..d9ab6b16960 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -220,12 +220,11 @@ class PulseCounter(GEMSensor): if self._sensor.pulses_per_second is None: return None - result = ( + return ( self._sensor.pulses_per_second * self._counted_quantity_per_pulse * self._seconds_per_time_unit ) - return result @property def _seconds_per_time_unit(self) -> int: diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 7657201da4d..a0f8d2b9a39 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -277,11 +277,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await group.async_update_tracked_entity_ids(entity_ids) if ATTR_NAME in service.data: - group.name = service.data[ATTR_NAME] + group.set_name(service.data[ATTR_NAME]) need_update = True if ATTR_ICON in service.data: - group.icon = service.data[ATTR_ICON] + group.set_icon(service.data[ATTR_ICON]) need_update = True if ATTR_ALL in service.data: diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index 24c10fd2e7b..a8fd9027984 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -12,6 +12,7 @@ from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -20,10 +21,7 @@ from homeassistant.core import ( 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 homeassistant.helpers.event import async_track_state_change_event from .const import ATTR_AUTO, ATTR_ORDER, DOMAIN, GROUP_ORDER, REG_KEY from .registry import GroupIntegrationRegistry @@ -152,9 +150,9 @@ class Group(Entity): This Object has factory function for creation. """ self.hass = hass - self._name = name + self._attr_name = name self._state: str | None = None - self._icon = icon + self._attr_icon = icon self._set_tracked(entity_ids) self._on_off: dict[str, bool] = {} self._assumed: dict[str, bool] = {} @@ -236,30 +234,18 @@ class Group(Entity): 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: + def set_name(self, value: str) -> None: """Set Group name.""" - self._name = value + self._attr_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: + def set_icon(self, value: str | None) -> None: """Set Icon for group.""" - self._icon = value + self._attr_icon = value @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index 61ddb3e0645..e5752a7835f 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -25,13 +25,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, 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.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .entity import GroupEntity diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index ccb7154f7c1..6c49f88a12f 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -45,13 +45,17 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + 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.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType KEY_ANNOUNCE = "announce" diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index bad3d7944d3..425dcf5a914 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -34,12 +34,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def add_defaults( - input_data: dict[str, Any], default_data: dict[str, Any] + input_data: dict[str, Any], default_data: Mapping[str, Any] ) -> dict[str, Any]: """Deep update a dictionary with default values.""" for key, val in default_data.items(): if isinstance(val, Mapping): - input_data[key] = add_defaults(input_data.get(key, {}), val) # type: ignore[arg-type] + input_data[key] = add_defaults(input_data.get(key, {}), val) elif key not in input_data: input_data[key] = val return input_data diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 1441d39d331..6cdb929d60c 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -47,10 +47,12 @@ def _process_group_platform( 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 __init__(self) -> None: + """Imitialize registry.""" + self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} + self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} + self.on_states_by_domain: dict[str, set[str]] = {} + self.exclude_domains: set[str] = set() def exclude_domain(self) -> None: """Exclude the current domain.""" diff --git a/homeassistant/components/group/util.py b/homeassistant/components/group/util.py index 14f5064290f..1ba8934d021 100644 --- a/homeassistant/components/group/util.py +++ b/homeassistant/components/group/util.py @@ -29,7 +29,7 @@ def mean_int(*args: Any) -> int: def mean_tuple(*args: Any) -> tuple[float | Any, ...]: """Return the mean values along the columns of the supplied values.""" - return tuple(sum(x) / len(x) for x in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args, strict=False)) def attribute_equal(states: list[State], key: str) -> bool: diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index d872474f1da..98ceb35ee17 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], - "requirements": ["growattServer==1.3.0"] + "requirements": ["growattServer==1.5.0"] } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 3cf1fa30c99..c41d3ac486f 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle, dt as dt_util @@ -46,8 +47,7 @@ def get_device_list(api, config): not login_response["success"] and login_response["msg"] == LOGIN_INVALID_AUTH_CODE ): - _LOGGER.error("Username, Password or URL may be incorrect!") - return + raise ConfigEntryError("Username, Password or URL may be incorrect!") user_id = login_response["user"]["id"] if plant_id == DEFAULT_PLANT_ID: plant_info = api.plant_list(user_id) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 9ee2aef40ba..9a8852b731d 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_API_USER, DEFAULT_URL, DOMAIN @@ -79,6 +80,20 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import habitica config from configuration.yaml.""" + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + breaks_in_ha_version="2024.11.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Habitica", + }, + ) return await self.async_step_user(import_data) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 992eaf52326..cdb31b4388c 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -48,17 +48,13 @@ class HarmonyData(HarmonySubscriberMixin): def activity_names(self) -> list[str]: """Names of all the remotes activities.""" activity_infos = self.activities - activities = [activity["label"] for activity in activity_infos] - - return activities + return [activity["label"] for activity in activity_infos] @property def device_names(self): """Names of all of the devices connected to the hub.""" device_infos = self._client.config.get("device", []) - devices = [device["label"] for device in device_infos] - - return devices + return [device["label"] for device in device_infos] @property def unique_id(self): diff --git a/homeassistant/components/harmony/entity.py b/homeassistant/components/harmony/entity.py index 99b5744e0ed..8bfa9fbad4d 100644 --- a/homeassistant/components/harmony/entity.py +++ b/homeassistant/components/harmony/entity.py @@ -6,6 +6,7 @@ from collections.abc import Callable from datetime import datetime import logging +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later @@ -38,7 +39,7 @@ class HarmonyEntity(Entity): _LOGGER.debug("%s: connected to the HUB", self._data.name) self.async_write_ha_state() - self._clear_disconnection_delay() + self._async_clear_disconnection_delay() async def async_got_disconnected(self, _: str | None = None) -> None: """Notification that we're disconnected from the HUB.""" @@ -46,15 +47,19 @@ class HarmonyEntity(Entity): # 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( - self.hass, TIME_MARK_DISCONNECTED, self._mark_disconnected_if_unavailable + self.hass, + TIME_MARK_DISCONNECTED, + self._async_mark_disconnected_if_unavailable, ) - def _clear_disconnection_delay(self) -> None: + @callback + def _async_clear_disconnection_delay(self) -> None: if self._unsub_mark_disconnected: self._unsub_mark_disconnected() self._unsub_mark_disconnected = None - def _mark_disconnected_if_unavailable(self, _: datetime) -> None: + @callback + def _async_mark_disconnected_if_unavailable(self, _: datetime) -> None: self._unsub_mark_disconnected = None if not self.available: # Still disconnected. Let the state engine know. diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index c6b2e9be718..0c9bdcb9c6e 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -138,7 +138,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): _LOGGER.debug("%s: Harmony Hub added", self._data.name) - self.async_on_remove(self._clear_disconnection_delay) + self.async_on_remove(self._async_clear_disconnection_delay) self._setup_callbacks() self.async_on_remove( diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 46ba00185f5..972942caf52 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -368,7 +368,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: require_admin=True, ) - await hassio.update_hass_api(config.get("http", {}), refresh_token) + update_hass_api_task = hass.async_create_task( + hassio.update_hass_api(config.get("http", {}), refresh_token), eager_start=True + ) last_timezone = None @@ -384,7 +386,7 @@ 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, run_immediately=True) + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config) push_config_task = hass.async_create_task(push_config(None), eager_start=True) # Start listening for problems with supervisor and making issues @@ -481,6 +483,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # _async_setup_hardware_integration is called # so the hardware integration can be set up # and does not fallback to calling later + await update_hass_api_task await panels_task await update_info_task await push_config_task diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index fcdcd38f776..674a828c3b8 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -381,7 +381,7 @@ class AddonManager: self._logger.error(err) break - return self._hass.async_create_task(addon_operation()) + return self._hass.async_create_task(addon_operation(), eager_start=False) class AddonError(HomeAssistantError): diff --git a/homeassistant/components/hassio/data.py b/homeassistant/components/hassio/data.py index eaa7c2431fe..3d684d6cd7c 100644 --- a/homeassistant/components/hassio/data.py +++ b/homeassistant/components/hassio/data.py @@ -371,9 +371,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has # 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 + for device in self.dev_reg.devices.get_devices_for_config_entry_id( + self.entry_id + ) + if 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) @@ -420,7 +421,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats() results = await asyncio.gather(*updates.values()) - for key, result in zip(updates, results): + for key, result in zip(updates, results, strict=False): data[key] = result _addon_data = data[DATA_SUPERVISOR_INFO].get("addons", []) @@ -483,28 +484,28 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has """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) + return (slug, None) + return (slug, stats) 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) + return (slug, None) + return (slug, changelog) 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) + return (slug, None) + return (slug, info) @callback def async_enable_container_updates( diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index ffb67730fa5..826c7a27b98 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -188,15 +188,13 @@ class HassIOView(HomeAssistantView): async for data, _ in client.content.iter_chunks(): await response.write(data) - return response - except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) - - except TimeoutError: + raise HTTPBadGateway from err + except TimeoutError as err: _LOGGER.error("Client timeout error on API request %s", path) - - raise HTTPBadGateway + raise HTTPBadGateway from err + return response get = _handle post = _handle diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6d6faa6fe75..ed6e47145dd 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -197,7 +197,6 @@ class HassIOIngress(HomeAssistantView): content_type or simple_response.content_type ): simple_response.enable_compression() - await simple_response.prepare(request) return simple_response # Stream response diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 70fc024c005..b32e5ebcd53 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -2,7 +2,7 @@ "domain": "hassio", "name": "Home Assistant Supervisor", "codeowners": ["@home-assistant/supervisor"], - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/hassio", "iot_class": "local_polling", "quality_scale": "internal" diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 63c1da4bfd8..6abf9ca6334 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -52,14 +52,14 @@ "fix_flow": { "step": { "fix_menu": { - "description": "`{reference}` is a filesystem with the name hassos-data and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the 'Rename' option to rename the filesystem to prevent this. Use the 'Adopt' option to make that your data disk and rename the existing one. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system.", + "description": "At `{reference}`, we detected another active data disk (containing a file system `hassos-data` from another Home Assistant installation).\n\nYou need to decide what to do with it. Otherwise Home Assistant might choose the wrong data disk at system reboot.\n\nIf you don't want to use this data disk, unplug it from your system. If you leave it plugged in, choose one of the following options:", "menu_options": { - "system_rename_data_disk": "Rename", - "system_adopt_data_disk": "Adopt" + "system_rename_data_disk": "Mark as inactive data disk (rename file system)", + "system_adopt_data_disk": "Use the detected data disk instead of the current system" } }, "system_adopt_data_disk": { - "description": "This fix will initiate a system reboot which will make Home Assistant and all the Add-ons inaccessible for a brief period. After the reboot `{reference}` will be the data disk of Home Assistant and your existing data disk will be renamed and ignored." + "description": "Select submit to make `{reference}` the active data disk. The one and only.\n\nYou won't have access anymore to the current Home Assistant data (will be marked as inactive data disk). After reboot, your system will be in the state of the Home Assistant data on `{reference}`." } }, "abort": { @@ -187,6 +187,10 @@ "unsupported_systemd_resolved": { "title": "Unsupported system - Systemd-Resolved issues", "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_virtualization_image": { + "title": "Unsupported system - Incorrect OS image for virtualization", + "description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this." } }, "entity": { diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index ed7c768e161..1573ff3f23e 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -478,7 +478,6 @@ class SourceManager: if controller.is_signed_in: favorites = await controller.get_favorites() inputs = await controller.get_input_sources() - return favorites, inputs except HeosError as error: if retry_attempts < self.max_retry_attempts: retry_attempts += 1 @@ -488,7 +487,9 @@ class SourceManager: await asyncio.sleep(self.retry_delay) else: _LOGGER.error("Unable to update sources: %s", error) - return + return None + else: + return favorites, inputs async def update_sources(event, data=None): if event in ( diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 9da1ce491f0..1b99ba64827 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b departure=departure, ) - cls: type[HERETransitDataUpdateCoordinator] | type[HERERoutingDataUpdateCoordinator] + cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator] if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: cls = HERETransitDataUpdateCoordinator else: diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index d27ea577c29..36d5c1efe1e 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -124,8 +124,8 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): await async_validate_api_key(user_input[CONF_API_KEY]) except HERERoutingUnauthorizedError: errors["base"] = "invalid_auth" - except (HERERoutingError, HERETransitError) as error: - _LOGGER.exception("Unexpected exception: %s", error) + except (HERERoutingError, HERETransitError): + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: self._config = user_input diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 462d8464229..465416607a2 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Callable, Iterable from dataclasses import dataclass from datetime import datetime as dt, timedelta import logging @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -31,7 +32,6 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_utc_time, async_track_state_change_event, ) @@ -173,7 +173,7 @@ async def ws_get_history_during_period( def _generate_stream_message( - states: MutableMapping[str, list[dict[str, Any]]], + states: dict[str, list[dict[str, Any]]], start_day: dt, end_day: dt, ) -> dict[str, Any]: @@ -201,7 +201,7 @@ def _generate_websocket_response( msg_id: int, start_time: dt, end_time: dt, - states: MutableMapping[str, list[dict[str, Any]]], + states: dict[str, list[dict[str, Any]]], ) -> bytes: """Generate a websocket response.""" return json_bytes( @@ -225,7 +225,7 @@ def _generate_historical_response( ) -> tuple[float, dt | None, bytes | None]: """Generate a historical response.""" states = cast( - MutableMapping[str, list[dict[str, Any]]], + dict[str, list[dict[str, Any]]], history.get_significant_states( hass, start_time, @@ -311,7 +311,7 @@ def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, An def _events_to_compressed_states( events: Iterable[Event], no_attributes: bool -) -> MutableMapping[str, list[dict[str, Any]]]: +) -> dict[str, list[dict[str, Any]]]: """Convert events to a compressed states.""" states_by_entity_ids: dict[str, list[dict[str, Any]]] = {} for event in events: diff --git a/homeassistant/components/history_stats/coordinator.py b/homeassistant/components/history_stats/coordinator.py index 2127f1d3dc5..159de11a9f1 100644 --- a/homeassistant/components/history_stats/coordinator.py +++ b/homeassistant/components/history_stats/coordinator.py @@ -6,12 +6,15 @@ from datetime import timedelta import logging from typing import Any -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.event import ( +from homeassistant.core import ( + CALLBACK_TYPE, + Event, EventStateChangedData, - async_track_state_change_event, + HomeAssistant, + callback, ) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.start import async_at_start from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 62ab28dc4f1..544e1772b01 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -6,8 +6,7 @@ from dataclasses import dataclass import datetime from homeassistant.components.recorder import get_instance, history -from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers.event import EventStateChangedData +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State from homeassistant.helpers.template import Template import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index ba672e38106..dec15e25b0b 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -95,10 +95,10 @@ class HitronCODADeviceScanner(DeviceScanner): return False try: self._userid = res.cookies["userid"] - return True except KeyError: _LOGGER.error("Failed to log in to router") return False + return True def _update_info(self): """Get ARP from router.""" diff --git a/homeassistant/components/hko/coordinator.py b/homeassistant/components/hko/coordinator.py index c7d80ae299e..566ba5dcf5e 100644 --- a/homeassistant/components/hko/coordinator.py +++ b/homeassistant/components/hko/coordinator.py @@ -101,7 +101,7 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): def _convert_current(self, data: dict[str, Any]) -> dict[str, Any]: """Return temperature and humidity in the appropriate format.""" - current = { + return { API_HUMIDITY: data[API_HUMIDITY][API_DATA][0][API_VALUE], API_TEMPERATURE: next( ( @@ -112,12 +112,11 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): 0, ), } - return current def _convert_forecast(self, data: dict[str, Any]) -> dict[str, Any]: """Return daily forecast in the appropriate format.""" date = data[API_FORECAST_DATE] - forecast = { + return { ATTR_FORECAST_CONDITION: self._convert_icon_condition( data[API_FORECAST_ICON], data[API_FORECAST_WEATHER] ), @@ -125,7 +124,6 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ATTR_FORECAST_TEMP_LOW: data[API_FORECAST_MIN_TEMP][API_VALUE], ATTR_FORECAST_TIME: f"{date[0:4]}-{date[4:6]}-{date[6:8]}T00:00:00+08:00", } - return forecast def _convert_icon_condition(self, icon_code: int, info: str) -> str: """Return the condition corresponding to an icon code.""" diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 5a1edcd3c3f..3494798b50b 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.46", "babel==2.13.1"] + "requirements": ["holidays==0.47", "babel==2.13.1"] } diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 135b2847520..4d6d9724ecb 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -259,7 +259,7 @@ class ExposedEntities: if assistant in registry_entry.options: if "should_expose" in registry_entry.options[assistant]: should_expose = registry_entry.options[assistant]["should_expose"] - return should_expose + return should_expose # noqa: RET504 if self.async_get_expose_new_entities(assistant): should_expose = self._is_default_exposed(entity_id, registry_entry) @@ -286,7 +286,7 @@ class ExposedEntities: ) and assistant in exposed_entity.assistants: if "should_expose" in exposed_entity.assistants[assistant]: should_expose = exposed_entity.assistants[assistant]["should_expose"] - return should_expose + return should_expose # noqa: RET504 if self.async_get_expose_new_entities(assistant): should_expose = self._is_default_exposed(entity_id, None) diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 12d6c66b69c..92a91dbd5cb 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ICON, @@ -11,10 +12,12 @@ from homeassistant.components.logbook import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.typing import NoEventData +from homeassistant.util.event_type import EventType from . import DOMAIN -EVENT_TO_NAME = { +EVENT_TO_NAME: dict[EventType[Any] | str, str] = { EVENT_HOMEASSISTANT_STOP: "stopped", EVENT_HOMEASSISTANT_START: "started", } @@ -23,12 +26,14 @@ EVENT_TO_NAME = { @callback def async_describe_events( hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], + async_describe_event: Callable[ + [str, EventType[NoEventData] | str, Callable[[Event], dict[str, str]]], None + ], ) -> None: """Describe logbook events.""" @callback - def async_describe_hass_event(event: Event) -> dict[str, str]: + def async_describe_hass_event(event: Event[NoEventData]) -> dict[str, str]: """Describe homeassistant logbook event.""" return { LOGBOOK_ENTRY_NAME: "Home Assistant", diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 37604c0e18e..09b2f17c947 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -191,6 +191,18 @@ }, "service_not_found": { "message": "Service {domain}.{service} not found." + }, + "service_does_not_support_response": { + "message": "A service which does not return responses can't be called with {return_response}." + }, + "service_lacks_response_request": { + "message": "The service call requires responses and must be called with {return_response}." + }, + "service_reponse_invalid": { + "message": "Failed to process the returned service response data, expected a dictionary, but got {response_data_type}." + }, + "service_should_be_blocking": { + "message": "A non blocking service call with argument {non_blocking_argument} can't be used together with argument {return_response}." } } } diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 85bd2708d5e..d29baf342ab 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -171,9 +171,7 @@ async def async_attach_trigger( 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=event_filter, run_immediately=True - ) + hass.bus.async_listen(event_type, handle_event, event_filter=event_filter) for event_type in event_types ] diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 2575af41401..43cc3d0918e 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -22,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -34,7 +35,6 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.event import ( - EventStateChangedData, async_track_same_state, async_track_state_change_event, ) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 6f3183e2b40..e0cbbf09610 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_A from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -24,7 +25,6 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.event import ( - EventStateChangedData, async_track_same_state, async_track_state_change_event, process_state_match, diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index b1d19d54795..6d035683f71 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -16,6 +16,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -23,7 +24,6 @@ from homeassistant.core import ( ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, async_track_time_change, diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 338d8679b19..b33bfe5ed1e 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -20,15 +20,18 @@ from homeassistant.helpers.issue_registry import ( async_create_issue, async_delete_issue, ) -from homeassistant.helpers.start import async_at_start +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.setup import EventComponentLoaded COMPONENT_LOADED_COOLDOWN = 30 DOMAIN = "homeassistant_alerts" UPDATE_INTERVAL = timedelta(hours=3) _LOGGER = logging.getLogger(__name__) +REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=30) + CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -51,7 +54,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: response = await async_get_clientsession(hass).get( f"https://alerts.home-assistant.io/alerts/{alert.alert_id}.json", - timeout=aiohttp.ClientTimeout(total=30), + timeout=REQUEST_TIMEOUT, ) except TimeoutError: _LOGGER.warning("Error fetching %s: timeout", alert.filename) @@ -84,7 +87,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not coordinator.last_update_success: return - hass.async_create_task(async_update_alerts()) + hass.async_create_background_task( + async_update_alerts(), "homeassistant_alerts update", eager_start=True + ) coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) @@ -96,18 +101,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: cooldown=COMPONENT_LOADED_COOLDOWN, immediate=False, function=coordinator.async_refresh, + background=True, ) @callback - def _component_loaded(_: Event) -> None: + def _component_loaded(_: Event[EventComponentLoaded]) -> None: refresh_debouncer.async_schedule_call() await coordinator.async_refresh() - hass.bus.async_listen( - EVENT_COMPONENT_LOADED, _component_loaded, run_immediately=True - ) + hass.bus.async_listen(EVENT_COMPONENT_LOADED, _component_loaded) - async_at_start(hass, initial_refresh) + async_at_started(hass, initial_refresh) return True @@ -147,7 +151,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) async def _async_update_data(self) -> dict[str, IntegrationAlert]: response = await async_get_clientsession(self.hass).get( "https://alerts.home-assistant.io/alerts.json", - timeout=aiohttp.ClientTimeout(total=10), + timeout=REQUEST_TIMEOUT, ) alerts = await response.json() diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 535d1706737..31032ff6a8c 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -417,6 +417,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.install_task = self.hass.async_create_task( multipan_manager.async_install_addon_waiting(), "SiLabs Multiprotocol addon install", + eager_start=False, ) if not self.install_task.done(): @@ -524,7 +525,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): if not self.start_task: self.start_task = self.hass.async_create_task( - multipan_manager.async_start_addon_waiting() + multipan_manager.async_start_addon_waiting(), eager_start=False ) if not self.start_task.done(): @@ -561,7 +562,8 @@ class OptionsFlowHandler(OptionsFlow, ABC): """Prepare info needed to complete the config entry update.""" # Always reload entry after installing the addon. self.hass.async_create_task( - self.hass.config_entries.async_reload(self.config_entry.entry_id) + self.hass.config_entries.async_reload(self.config_entry.entry_id), + eager_start=False, ) # Finish ZHA migration if needed @@ -721,6 +723,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.install_task = self.hass.async_create_task( flasher_manager.async_install_addon_waiting(), "SiLabs Flasher addon install", + eager_start=False, ) if not self.install_task.done(): @@ -811,6 +814,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.stop_task = self.hass.async_create_task( multipan_manager.async_uninstall_addon_waiting(), "SiLabs Multiprotocol addon uninstall", + eager_start=False, ) if not self.stop_task.done(): @@ -843,7 +847,9 @@ class OptionsFlowHandler(OptionsFlow, ABC): AddonState.NOT_RUNNING ) - self.start_task = self.hass.async_create_task(start_and_wait_until_done()) + self.start_task = self.hass.async_create_task( + start_and_wait_until_done(), eager_start=False + ) if not self.start_task.done(): return self.async_show_progress( diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 1ee4710769b..fc02f31f263 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -2,85 +2,62 @@ from __future__ import annotations -from homeassistant.components import usb -from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( - check_multi_pan_addon, - get_zigbee_socket, - multi_pan_addon_using_device, -) -from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import discovery_flow +import logging -from .const import DOMAIN -from .util import get_hardware_variant, get_usb_service_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from .util import guess_firmware_type -async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Finish Home Assistant SkyConnect config entry setup.""" - matcher = usb.USBCallbackMatcher( - domain=DOMAIN, - vid=entry.data["vid"].upper(), - pid=entry.data["pid"].upper(), - serial_number=entry.data["serial_number"].lower(), - manufacturer=entry.data["manufacturer"].lower(), - description=entry.data["description"].lower(), - ) - - if not usb.async_is_plugged_in(hass, matcher): - # The USB dongle is not plugged in, remove the config entry - hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) - return - - usb_dev = entry.data["device"] - # The call to get_serial_by_id can be removed in HA Core 2024.1 - dev_path = await hass.async_add_executor_job(usb.get_serial_by_id, usb_dev) - - if not await multi_pan_addon_using_device(hass, dev_path): - usb_info = get_usb_service_info(entry) - await hass.config_entries.flow.async_init( - "zha", - context={"source": "usb"}, - data=usb_info, - ) - return - - hw_variant = get_hardware_variant(entry) - hw_discovery_data = { - "name": f"{hw_variant.short_name} Multiprotocol", - "port": { - "path": get_zigbee_socket(), - }, - "radio_type": "ezsp", - } - discovery_flow.async_create_flow( - hass, - "zha", - context={"source": SOURCE_HARDWARE}, - data=hw_discovery_data, - ) +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant SkyConnect config entry.""" - - try: - await check_multi_pan_addon(hass) - except HomeAssistantError as err: - raise ConfigEntryNotReady from err - - @callback - def async_usb_scan_done() -> None: - """Handle usb discovery started.""" - hass.async_create_task(_async_usb_scan_done(hass, entry)) - - unsub_usb = usb.async_register_initial_scan_callback(hass, async_usb_scan_done) - entry.async_on_unload(unsub_usb) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return True + + +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 == 1: + if config_entry.minor_version == 1: + # Add-on startup with type service get started before Core, always (e.g. the + # Multi-Protocol add-on). Probing the firmware would interfere with the add-on, + # so we can't safely probe here. Instead, we must make an educated guess! + firmware_guess = await guess_firmware_type( + hass, config_entry.data["device"] + ) + + new_data = {**config_entry.data} + new_data["firmware"] = firmware_guess.firmware_type.value + + # Copy `description` to `product` + new_data["product"] = new_data["description"] + + hass.config_entries.async_update_entry( + config_entry, + data=new_data, + version=1, + minor_version=2, + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True + + # This means the user has downgraded from a future version + return False diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 3a3d32c2888..9d0aa902cc4 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -2,29 +2,498 @@ from __future__ import annotations +from abc import ABC, abstractmethod +import asyncio +import logging from typing import Any +from universal_silabs_flasher.const import ApplicationType + from homeassistant.components import usb +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + is_hassio, +) from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.components.zha.repairs.wrong_silabs_firmware import ( + probe_silabs_firmware_type, +) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryBaseFlow, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow -from .const import DOMAIN, HardwareVariant -from .util import get_hardware_variant, get_usb_service_info +from .const import DOCS_WEB_FLASHER_URL, DOMAIN, ZHA_DOMAIN, HardwareVariant +from .util import ( + get_hardware_variant, + get_otbr_addon_manager, + get_usb_service_info, + get_zha_device_path, + get_zigbee_flasher_addon_manager, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread" +STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee" -class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): +class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): + """Base flow to install firmware.""" + + _failed_addon_name: str + _failed_addon_reason: str + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate base flow.""" + super().__init__(*args, **kwargs) + + self._usb_info: usb.UsbServiceInfo | None = None + self._hw_variant: HardwareVariant | None = None + self._probed_firmware_type: ApplicationType | None = None + + self.addon_install_task: asyncio.Task | None = None + self.addon_start_task: asyncio.Task | None = None + self.addon_uninstall_task: asyncio.Task | None = None + + def _get_translation_placeholders(self) -> dict[str, str]: + """Shared translation placeholders.""" + placeholders = { + "model": ( + self._hw_variant.full_name + if self._hw_variant is not None + else "unknown" + ), + "firmware_type": ( + self._probed_firmware_type.value + if self._probed_firmware_type is not None + else "unknown" + ), + "docs_web_flasher_url": DOCS_WEB_FLASHER_URL, + } + + self.context["title_placeholders"] = placeholders + + return placeholders + + async def _async_set_addon_config( + self, config: dict, addon_manager: AddonManager + ) -> None: + """Set add-on config.""" + try: + await addon_manager.async_set_addon_options(config) + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow( + "addon_set_config_failed", + description_placeholders=self._get_translation_placeholders(), + ) from err + + async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo: + """Return add-on info.""" + try: + addon_info = await addon_manager.async_get_addon_info() + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow( + "addon_info_failed", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + ) from err + + return addon_info + + async def async_step_pick_firmware( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread or Zigbee firmware.""" + assert self._usb_info is not None + + self._probed_firmware_type = await probe_silabs_firmware_type( + self._usb_info.device, + probe_methods=( + # We probe in order of frequency: Zigbee, Thread, then multi-PAN + ApplicationType.GECKO_BOOTLOADER, + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ), + ) + + if self._probed_firmware_type not in ( + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ): + return self.async_abort( + reason="unsupported_firmware", + description_placeholders=self._get_translation_placeholders(), + ) + + return self.async_show_menu( + step_id="pick_firmware", + menu_options=[ + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, + ], + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware.""" + # Allow the stick to be used with ZHA without flashing + if self._probed_firmware_type == ApplicationType.EZSP: + return await self.async_step_confirm_zigbee() + + if not is_hassio(self.hass): + return self.async_abort( + reason="not_hassio", + description_placeholders=self._get_translation_placeholders(), + ) + + # Only flash new firmware if we need to + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(fw_flasher_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_zigbee_flasher_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_run_zigbee_flasher_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="addon_already_running", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + ) + + async def async_step_install_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show progress dialog for installing the Zigbee flasher addon.""" + return await self._install_addon( + get_zigbee_flasher_addon_manager(self.hass), + "install_zigbee_flasher_addon", + "run_zigbee_flasher_addon", + ) + + async def _install_addon( + self, + addon_manager: silabs_multiprotocol_addon.WaitingAddonManager, + step_id: str, + next_step_id: str, + ) -> ConfigFlowResult: + """Show progress dialog for installing an addon.""" + addon_info = await self._async_get_addon_info(addon_manager) + + _LOGGER.debug("Flasher addon state: %s", addon_info) + + if not self.addon_install_task: + self.addon_install_task = self.hass.async_create_task( + addon_manager.async_install_addon_waiting(), + "Addon install", + ) + + if not self.addon_install_task.done(): + return self.async_show_progress( + step_id=step_id, + progress_action="install_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + progress_task=self.addon_install_task, + ) + + try: + await self.addon_install_task + except AddonError as err: + _LOGGER.error(err) + self._failed_addon_name = addon_manager.addon_name + self._failed_addon_reason = "addon_install_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_install_task = None + + return self.async_show_progress_done(next_step_id=next_step_id) + + async def async_step_addon_operation_failed( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Abort when add-on installation or start failed.""" + return self.async_abort( + reason=self._failed_addon_reason, + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": self._failed_addon_name, + }, + ) + + async def async_step_run_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Configure the flasher addon to point to the SkyConnect and run it.""" + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(fw_flasher_manager) + + assert self._usb_info is not None + new_addon_config = { + **addon_info.options, + "device": self._usb_info.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + + _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, fw_flasher_manager) + + if not self.addon_start_task: + + async def start_and_wait_until_done() -> None: + await fw_flasher_manager.async_start_addon_waiting() + # Now that the addon is running, wait for it to finish + await fw_flasher_manager.async_wait_until_addon_state( + AddonState.NOT_RUNNING + ) + + self.addon_start_task = self.hass.async_create_task( + start_and_wait_until_done() + ) + + if not self.addon_start_task.done(): + return self.async_show_progress( + step_id="run_zigbee_flasher_addon", + progress_action="run_zigbee_flasher_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + progress_task=self.addon_start_task, + ) + + try: + await self.addon_start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + self._failed_addon_name = fw_flasher_manager.addon_name + self._failed_addon_reason = "addon_start_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_start_task = None + + return self.async_show_progress_done( + next_step_id="uninstall_zigbee_flasher_addon" + ) + + async def async_step_uninstall_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Uninstall the flasher addon.""" + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + + if not self.addon_uninstall_task: + _LOGGER.debug("Uninstalling flasher addon") + self.addon_uninstall_task = self.hass.async_create_task( + fw_flasher_manager.async_uninstall_addon_waiting() + ) + + if not self.addon_uninstall_task.done(): + return self.async_show_progress( + step_id="uninstall_zigbee_flasher_addon", + progress_action="uninstall_zigbee_flasher_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + progress_task=self.addon_uninstall_task, + ) + + try: + await self.addon_uninstall_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + # The uninstall failing isn't critical so we can just continue + finally: + self.addon_uninstall_task = None + + return self.async_show_progress_done(next_step_id="confirm_zigbee") + + async def async_step_confirm_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Zigbee setup.""" + assert self._usb_info is not None + assert self._hw_variant is not None + self._probed_firmware_type = ApplicationType.EZSP + + if user_input is not None: + await self.hass.config_entries.flow.async_init( + ZHA_DOMAIN, + context={"source": "hardware"}, + data={ + "name": self._hw_variant.full_name, + "port": { + "path": self._usb_info.device, + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + }, + ) + + return self._async_flow_finished() + + return self.async_show_form( + step_id="confirm_zigbee", + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_thread( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware.""" + # We install the OTBR addon no matter what, since it is required to use Thread + if not is_hassio(self.hass): + return self.async_abort( + reason="not_hassio_thread", + description_placeholders=self._get_translation_placeholders(), + ) + + otbr_manager = get_otbr_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(otbr_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_otbr_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_start_otbr_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="otbr_addon_already_running", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": otbr_manager.addon_name, + }, + ) + + async def async_step_install_otbr_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show progress dialog for installing the OTBR addon.""" + return await self._install_addon( + get_otbr_addon_manager(self.hass), "install_otbr_addon", "start_otbr_addon" + ) + + async def async_step_start_otbr_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Configure OTBR to point to the SkyConnect and run the addon.""" + otbr_manager = get_otbr_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(otbr_manager) + + assert self._usb_info is not None + new_addon_config = { + **addon_info.options, + "device": self._usb_info.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + + _LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, otbr_manager) + + if not self.addon_start_task: + self.addon_start_task = self.hass.async_create_task( + otbr_manager.async_start_addon_waiting() + ) + + if not self.addon_start_task.done(): + return self.async_show_progress( + step_id="start_otbr_addon", + progress_action="start_otbr_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": otbr_manager.addon_name, + }, + progress_task=self.addon_start_task, + ) + + try: + await self.addon_start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + self._failed_addon_name = otbr_manager.addon_name + self._failed_addon_reason = "addon_start_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_start_task = None + + return self.async_show_progress_done(next_step_id="confirm_otbr") + + async def async_step_confirm_otbr( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm OTBR setup.""" + assert self._usb_info is not None + assert self._hw_variant is not None + + self._probed_firmware_type = ApplicationType.SPINEL + + if user_input is not None: + # OTBR discovery is done automatically via hassio + return self._async_flow_finished() + + return self.async_show_form( + step_id="confirm_otbr", + description_placeholders=self._get_translation_placeholders(), + ) + + @abstractmethod + def _async_flow_finished(self) -> ConfigFlowResult: + """Finish the flow.""" + # This should be implemented by a subclass + raise NotImplementedError + + +class HomeAssistantSkyConnectConfigFlow( + BaseFirmwareInstallFlow, ConfigFlow, domain=DOMAIN +): """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 + MINOR_VERSION = 2 @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, - ) -> HomeAssistantSkyConnectOptionsFlow: + ) -> OptionsFlow: """Return the options flow.""" - return HomeAssistantSkyConnectOptionsFlow(config_entry) + firmware_type = ApplicationType(config_entry.data["firmware"]) + + if firmware_type is ApplicationType.CPC: + return HomeAssistantSkyConnectMultiPanOptionsFlowHandler(config_entry) + + return HomeAssistantSkyConnectOptionsFlowHandler(config_entry) async def async_step_usb( self, discovery_info: usb.UsbServiceInfo @@ -37,27 +506,62 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): manufacturer = discovery_info.manufacturer description = discovery_info.description 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}) + discovery_info.device = await self.hass.async_add_executor_job( + usb.get_serial_by_id, discovery_info.device + ) + + self._usb_info = discovery_info + assert description is not None - hw_variant = HardwareVariant.from_usb_product_name(description) + self._hw_variant = HardwareVariant.from_usb_product_name(description) + + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm a discovery.""" + self._set_confirm_only() + + # Without confirmation, discovery can automatically progress into parts of the + # config flow logic that interacts with hardware. + if user_input is not None: + return await self.async_step_pick_firmware() + + return self.async_show_form( + step_id="confirm", + description_placeholders=self._get_translation_placeholders(), + ) + + def _async_flow_finished(self) -> ConfigFlowResult: + """Create the config entry.""" + assert self._usb_info is not None + assert self._hw_variant is not None + assert self._probed_firmware_type is not None return self.async_create_entry( - title=hw_variant.full_name, + title=self._hw_variant.full_name, data={ - "device": device, - "vid": vid, - "pid": pid, - "serial_number": serial_number, - "manufacturer": manufacturer, - "description": description, + "vid": self._usb_info.vid, + "pid": self._usb_info.pid, + "serial_number": self._usb_info.serial_number, + "manufacturer": self._usb_info.manufacturer, + "description": self._usb_info.description, # For backwards compatibility + "product": self._usb_info.description, + "device": self._usb_info.device, + "firmware": self._probed_firmware_type.value, }, ) -class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): - """Handle an option flow for Home Assistant SkyConnect.""" +class HomeAssistantSkyConnectMultiPanOptionsFlowHandler( + silabs_multiprotocol_addon.OptionsFlowHandler +): + """Multi-PAN options flow for Home Assistant SkyConnect.""" async def _async_serial_port_settings( self, @@ -92,3 +596,112 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH def _hardware_name(self) -> str: """Return the name of the hardware.""" return self._hw_variant.full_name + + async def async_step_flashing_complete( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Finish flashing and update the config entry.""" + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + **self.config_entry.data, + "firmware": ApplicationType.EZSP.value, + }, + options=self.config_entry.options, + ) + + return await super().async_step_flashing_complete(user_input) + + +class HomeAssistantSkyConnectOptionsFlowHandler( + BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry +): + """Zigbee and Thread options flow handlers.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate options flow.""" + super().__init__(*args, **kwargs) + + self._usb_info = get_usb_service_info(self.config_entry) + self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"]) + self._hw_variant = HardwareVariant.from_usb_product_name( + self.config_entry.data["product"] + ) + + # Make `context` a regular dictionary + self.context = {} + + # Regenerate the translation placeholders + self._get_translation_placeholders() + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options flow.""" + # Don't probe the running firmware, we load it from the config entry + return self.async_show_menu( + step_id="pick_firmware", + menu_options=[ + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, + ], + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware.""" + assert self._usb_info is not None + + if is_hassio(self.hass): + otbr_manager = get_otbr_addon_manager(self.hass) + otbr_addon_info = await self._async_get_addon_info(otbr_manager) + + if ( + otbr_addon_info.state != AddonState.NOT_INSTALLED + and otbr_addon_info.options.get("device") == self._usb_info.device + ): + raise AbortFlow( + "otbr_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) + + return await super().async_step_pick_firmware_zigbee(user_input) + + async def async_step_pick_firmware_thread( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware.""" + assert self._usb_info is not None + + zha_entries = self.hass.config_entries.async_entries( + ZHA_DOMAIN, + include_ignore=False, + include_disabled=True, + ) + + if zha_entries and get_zha_device_path(zha_entries[0]) == self._usb_info.device: + raise AbortFlow( + "zha_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) + + return await super().async_step_pick_firmware_thread(user_input) + + def _async_flow_finished(self) -> ConfigFlowResult: + """Create the config entry.""" + assert self._usb_info is not None + assert self._hw_variant is not None + assert self._probed_firmware_type is not None + + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + **self.config_entry.data, + "firmware": self._probed_firmware_type.value, + }, + options=self.config_entry.options, + ) + + return self.async_create_entry(title="", data={}) diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py index 1dd1471c470..1d6c16dc528 100644 --- a/homeassistant/components/homeassistant_sky_connect/const.py +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -5,6 +5,17 @@ import enum from typing import Self DOMAIN = "homeassistant_sky_connect" +ZHA_DOMAIN = "zha" + +DOCS_WEB_FLASHER_URL = "https://skyconnect.home-assistant.io/firmware-update/" + +OTBR_ADDON_NAME = "OpenThread Border Router" +OTBR_ADDON_MANAGER_DATA = "openthread_border_router" +OTBR_ADDON_SLUG = "core_openthread_border_router" + +ZIGBEE_FLASHER_ADDON_NAME = "Silicon Labs Flasher" +ZIGBEE_FLASHER_ADDON_MANAGER_DATA = "silabs_flasher" +ZIGBEE_FLASHER_ADDON_SLUG = "core_silabs_flasher" @dataclasses.dataclass(frozen=True) diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index a9abeb27737..2872077111a 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -25,7 +25,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: pid=entry.data["pid"], serial_number=entry.data["serial_number"], manufacturer=entry.data["manufacturer"], - description=entry.data["description"], + description=entry.data["product"], ), name=get_hardware_variant(entry).full_name, url=DOCUMENTATION_URL, diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 825649ef0d3..792406dcb02 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -57,6 +57,50 @@ "start_flasher_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]", "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]" + }, + "confirm": { + "title": "[%key:component::homeassistant_sky_connect::config::step::confirm::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::confirm::description%]" + }, + "pick_firmware": { + "title": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::description%]", + "menu_options": { + "pick_firmware_thread": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::menu_options::pick_firmware_zigbee%]" + } + }, + "install_zigbee_flasher_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::install_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::install_zigbee_flasher_addon::description%]" + }, + "run_zigbee_flasher_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::run_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::run_zigbee_flasher_addon::description%]" + }, + "zigbee_flasher_failed": { + "title": "[%key:component::homeassistant_sky_connect::config::step::zigbee_flasher_failed::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::zigbee_flasher_failed::description%]" + }, + "confirm_zigbee": { + "title": "[%key:component::homeassistant_sky_connect::config::step::confirm_zigbee::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::confirm_zigbee::description%]" + }, + "install_otbr_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::install_otbr_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::install_otbr_addon::description%]" + }, + "start_otbr_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::start_otbr_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::start_otbr_addon::description%]" + }, + "otbr_failed": { + "title": "[%key:component::homeassistant_sky_connect::config::step::otbr_failed::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::otbr_failed::description%]" + }, + "confirm_otbr": { + "title": "[%key:component::homeassistant_sky_connect::config::step::confirm_otbr::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::confirm_otbr::description%]" } }, "error": { @@ -68,12 +112,92 @@ "addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]", "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" + "not_hassio_thread": "[%key:component::homeassistant_sky_connect::config::abort::not_hassio_thread%]", + "otbr_addon_already_running": "[%key:component::homeassistant_sky_connect::config::abort::otbr_addon_already_running%]", + "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", + "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again." }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", - "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]" + "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "install_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::install_zigbee_flasher_addon%]", + "run_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::run_zigbee_flasher_addon%]", + "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::uninstall_zigbee_flasher_addon%]" + } + }, + "config": { + "flow_title": "{model}", + "step": { + "confirm": { + "title": "Set up the {model}", + "description": "The {model} can be used as either a Thread border router or a Zigbee coordinator. In the next step, you will choose which firmware will be configured." + }, + "pick_firmware": { + "title": "Pick your firmware", + "description": "The {model} can be used as a Thread border router or a Zigbee coordinator.", + "menu_options": { + "pick_firmware_thread": "Use as a Thread border router", + "pick_firmware_zigbee": "Use as a Zigbee coordinator" + } + }, + "install_zigbee_flasher_addon": { + "title": "Installing flasher", + "description": "Installing the Silicon Labs Flasher add-on." + }, + "run_zigbee_flasher_addon": { + "title": "Installing Zigbee firmware", + "description": "Installing Zigbee firmware. This will take about a minute." + }, + "uninstall_zigbee_flasher_addon": { + "title": "Removing flasher", + "description": "Removing the Silicon Labs Flasher add-on." + }, + "zigbee_flasher_failed": { + "title": "Zigbee installation failed", + "description": "The Zigbee firmware installation process was unsuccessful. Ensure no other software is trying to communicate with the {model} and try again." + }, + "confirm_zigbee": { + "title": "Zigbee setup complete", + "description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration once you exit." + }, + "install_otbr_addon": { + "title": "Installing OpenThread Border Router add-on", + "description": "The OpenThread Border Router (OTBR) add-on is being installed." + }, + "start_otbr_addon": { + "title": "Starting OpenThread Border Router add-on", + "description": "The OpenThread Border Router (OTBR) add-on is now starting." + }, + "otbr_failed": { + "title": "Failed to setup OpenThread Border Router", + "description": "The OpenThread Border Router add-on installation was unsuccessful. Ensure no other software is trying to communicate with the {model}, you have access to the internet and can install other add-ons, and try again. Check the Supervisor logs if the problem persists." + }, + "confirm_otbr": { + "title": "OpenThread Border Router setup complete", + "description": "Your {model} is now an OpenThread Border Router and will show up in the Thread integration once you exit." + } + }, + "abort": { + "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]", + "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.", + "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again." + }, + "progress": { + "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", + "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.", + "run_zigbee_flasher_addon": "Please wait while Zigbee firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes.", + "uninstall_zigbee_flasher_addon": "The Silicon Labs Flasher addon is being removed." } } } diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index e1de1d3b442..f242416fa9a 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -2,10 +2,35 @@ from __future__ import annotations -from homeassistant.components import usb -from homeassistant.config_entries import ConfigEntry +from collections import defaultdict +from dataclasses import dataclass +import logging +from typing import cast -from .const import HardwareVariant +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components import usb +from homeassistant.components.hassio import AddonError, AddonState, is_hassio +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + WaitingAddonManager, + get_multiprotocol_addon_manager, +) +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +from .const import ( + OTBR_ADDON_MANAGER_DATA, + OTBR_ADDON_NAME, + OTBR_ADDON_SLUG, + ZHA_DOMAIN, + ZIGBEE_FLASHER_ADDON_MANAGER_DATA, + ZIGBEE_FLASHER_ADDON_NAME, + ZIGBEE_FLASHER_ADDON_SLUG, + HardwareVariant, +) + +_LOGGER = logging.getLogger(__name__) def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: @@ -16,10 +41,115 @@ def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: pid=config_entry.data["pid"], serial_number=config_entry.data["serial_number"], manufacturer=config_entry.data["manufacturer"], - description=config_entry.data["description"], + description=config_entry.data["product"], ) 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"]) + return HardwareVariant.from_usb_product_name(config_entry.data["product"]) + + +def get_zha_device_path(config_entry: ConfigEntry) -> str: + """Get the device path from a ZHA config entry.""" + return cast(str, config_entry.data["device"]["path"]) + + +@singleton(OTBR_ADDON_MANAGER_DATA) +@callback +def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the OTBR add-on manager.""" + return WaitingAddonManager( + hass, + _LOGGER, + OTBR_ADDON_NAME, + OTBR_ADDON_SLUG, + ) + + +@singleton(ZIGBEE_FLASHER_ADDON_MANAGER_DATA) +@callback +def get_zigbee_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the flasher add-on manager.""" + return WaitingAddonManager( + hass, + _LOGGER, + ZIGBEE_FLASHER_ADDON_NAME, + ZIGBEE_FLASHER_ADDON_SLUG, + ) + + +@dataclass(slots=True, kw_only=True) +class FirmwareGuess: + """Firmware guess.""" + + is_running: bool + firmware_type: ApplicationType + source: str + + +async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> FirmwareGuess: + """Guess the firmware type based on installed addons and other integrations.""" + device_guesses: defaultdict[str | None, list[FirmwareGuess]] = defaultdict(list) + + for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN): + zha_path = get_zha_device_path(zha_config_entry) + device_guesses[zha_path].append( + FirmwareGuess( + is_running=(zha_config_entry.state == ConfigEntryState.LOADED), + firmware_type=ApplicationType.EZSP, + source="zha", + ) + ) + + if is_hassio(hass): + otbr_addon_manager = get_otbr_addon_manager(hass) + + try: + otbr_addon_info = await otbr_addon_manager.async_get_addon_info() + except AddonError: + pass + else: + if otbr_addon_info.state != AddonState.NOT_INSTALLED: + otbr_path = otbr_addon_info.options.get("device") + device_guesses[otbr_path].append( + FirmwareGuess( + is_running=(otbr_addon_info.state == AddonState.RUNNING), + firmware_type=ApplicationType.SPINEL, + source="otbr", + ) + ) + + multipan_addon_manager = await get_multiprotocol_addon_manager(hass) + + try: + multipan_addon_info = await multipan_addon_manager.async_get_addon_info() + except AddonError: + pass + else: + if multipan_addon_info.state != AddonState.NOT_INSTALLED: + multipan_path = multipan_addon_info.options.get("device") + device_guesses[multipan_path].append( + FirmwareGuess( + is_running=(multipan_addon_info.state == AddonState.RUNNING), + firmware_type=ApplicationType.CPC, + source="multiprotocol", + ) + ) + + # Fall back to EZSP if we can't guess the firmware type + if device_path not in device_guesses: + return FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + ) + + # Prioritizes guesses that were pulled from a running addon or integration but keep + # the sort order we defined above + guesses = sorted( + device_guesses[device_path], + key=lambda guess: guess.is_running, + ) + + assert guesses + + return guesses[-1] diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 294dc7f33a6..f9f91ec162b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -351,9 +351,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) ) entry_data = HomeKitEntryData( @@ -953,9 +951,8 @@ class HomeKit: """Purge bridges that exist from failed pairing or manual resets.""" devices_to_purge = [ entry.id - for entry in dev_reg.devices.values() - if self._entry_id in entry.config_entries - and ( + for entry in dev_reg.devices.get_devices_for_config_entry_id(self._entry_id) + if ( identifier not in entry.identifiers # type: ignore[comparison-overlap] or connection not in entry.connections ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 39fa62e3445..40e86efe6a9 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -45,16 +45,15 @@ from homeassistant.core import ( CALLBACK_TYPE, Context, Event, + EventStateChangedData, + HassJobType, HomeAssistant, State, callback as ha_callback, split_entity_id, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.decorator import Registry from .const import ( @@ -438,7 +437,10 @@ class HomeAccessory(Accessory): # type: ignore[misc] self._update_available_from_state(state) self._subscriptions.append( async_track_state_change_event( - self.hass, [self.entity_id], self.async_update_event_state_callback + self.hass, + [self.entity_id], + self.async_update_event_state_callback, + job_type=HassJobType.Callback, ) ) @@ -458,6 +460,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] self.hass, [self.linked_battery_sensor], self.async_update_linked_battery_callback, + job_type=HassJobType.Callback, ) ) elif state is not None: @@ -470,6 +473,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] self.hass, [self.linked_battery_charging_sensor], self.async_update_linked_battery_charging_callback, + job_type=HassJobType.Callback, ) ) elif battery_charging_state is None and state is not None: diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index d47d9775ed2..4f05bfbd687 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -17,12 +17,19 @@ 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 Event, HomeAssistant, State, callback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, + HassJobType, + HomeAssistant, + State, + callback, +) +from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, ) +from homeassistant.util.async_ import create_eager_task from .accessories import TYPES, HomeAccessory, HomeDriver from .const import ( @@ -266,6 +273,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self.hass, [self.linked_motion_sensor], self._async_update_motion_state_event, + job_type=HassJobType.Callback, ) ) @@ -276,6 +284,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self.hass, [self.linked_doorbell_sensor], self._async_update_doorbell_state_event, + job_type=HassJobType.Callback, ) ) @@ -426,7 +435,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] async def watch_session(_: Any) -> None: await self._async_ffmpeg_watch(session_info["id"]) - session_info[FFMPEG_LOGGER] = asyncio.create_task( + session_info[FFMPEG_LOGGER] = create_eager_task( self._async_log_stderr_stream(stderr_reader) ) session_info[FFMPEG_WATCHER] = async_track_time_interval( @@ -494,11 +503,12 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] _LOGGER.info("[%s] %s stream", session_id, shutdown_method) try: await getattr(stream, shutdown_method)() - return except Exception: # pylint: disable=broad-except _LOGGER.exception( "[%s] Failed to %s stream", session_id, shutdown_method ) + else: + return async def reconfigure_stream( self, session_info: dict[str, Any], stream_config: dict[str, Any] diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 2452fd65026..29dda418665 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -34,11 +34,14 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import Event, State, callback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, - async_track_state_change_event, + HassJobType, + State, + callback, ) +from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory from .const import ( @@ -139,6 +142,7 @@ class GarageDoorOpener(HomeAccessory): self.hass, [self.linked_obstruction_sensor], self._async_update_obstruction_event, + job_type=HassJobType.Callback, ) ) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 0dee5fa2b71..64c121878a9 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -125,11 +125,9 @@ class Fan(HomeAccessory): ), ) - setter_callback = ( - lambda value, preset_mode=preset_mode: self.set_preset_mode( - value, preset_mode - ) - ) + def setter_callback(value: int, preset_mode: str = preset_mode) -> None: + return self.set_preset_mode(value, preset_mode) + self.preset_mode_chars[preset_mode] = preset_serv.configure_char( CHAR_ON, value=False, diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 2b4de072b6a..5bdf5950f18 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -25,11 +25,14 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, State, callback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, - async_track_state_change_event, + HassJobType, + State, + callback, ) +from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory from .const import ( @@ -187,6 +190,7 @@ class HumidifierDehumidifier(HomeAccessory): self.hass, [self.linked_humidity_sensor], self.async_update_current_humidity_event, + job_type=HassJobType.Callback, ) ) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ba0f9790efb..5dc520e8568 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -411,10 +411,10 @@ class Thermostat(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) - def _temperature_to_homekit(self, temp: float | int) -> float: + def _temperature_to_homekit(self, temp: float) -> float: return temperature_to_homekit(temp, self._unit) - def _temperature_to_states(self, temp: float | int) -> float: + def _temperature_to_states(self, temp: float) -> float: return temperature_to_states(temp, self._unit) def _set_chars(self, char_values: dict[str, Any]) -> None: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 031cdbbc9bd..dec7fe8eba7 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -38,9 +38,15 @@ from homeassistant.const import ( CONF_TYPE, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + Event, + EventStateChangedData, + 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.util.unit_conversion import TemperatureConverter @@ -395,14 +401,14 @@ def cleanup_name_for_homekit(name: str | None) -> str: return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] -def temperature_to_homekit(temperature: float | int, unit: str) -> float: +def temperature_to_homekit(temperature: float, unit: str) -> float: """Convert temperature to Celsius for HomeKit.""" return round( TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS), 1 ) -def temperature_to_states(temperature: float | int, unit: str) -> float: +def temperature_to_states(temperature: float, unit: str) -> float: """Convert temperature back from Celsius to Home Assistant unit.""" return ( round( @@ -572,11 +578,12 @@ def _async_find_next_available_port(start_port: int, exclude_ports: set) -> int: continue try: test_socket.bind(("", port)) - return port except OSError: if port == MAX_PORT: raise continue + else: + return port raise RuntimeError("unreachable") @@ -584,10 +591,9 @@ def pid_is_alive(pid: int) -> bool: """Check to see if a process is alive.""" try: os.kill(pid, 0) - return True except OSError: - pass - return False + return False + return True def accessory_friendly_name(hass_name: str, accessory: Accessory) -> str: diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 218094ddaf5..639cec6dcb5 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -86,9 +86,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) return True diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 49ae3bb4a42..544e23798d0 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -2,8 +2,9 @@ from __future__ import annotations +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Final +from typing import Any, Final from aiohomekit.model.characteristics import ( ActivationStateValues, @@ -49,12 +50,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) # Map of Homekit operation modes to hass modes diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 883ec4f1a44..78beb7bfffa 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -286,7 +286,6 @@ class HKDevice: self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STARTED, self._async_populate_ble_accessory_state, - run_immediately=True, ) ) else: @@ -384,6 +383,7 @@ class HKDevice: model=accessory.model, sw_version=accessory.firmware_revision, hw_version=accessory.hardware_revision, + serial_number=accessory.serial_number, ) if accessory.aid != 1: diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index b15ce645a29..ca041d49e11 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -29,12 +30,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - STATE_STOPPED = "stopped" CURRENT_GARAGE_STATE_MAP = { diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 136c063f280..c5478ccb97d 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib from typing import Any from aiohomekit.model.characteristics import ( @@ -80,11 +79,7 @@ class HomeKitEntity(Entity): def _async_clear_property_cache(self, properties: tuple[str, ...]) -> None: """Clear the cache of properties.""" for prop in properties: - # suppress is slower than try-except-pass, but - # we do not expect to have many properties to clear - # or this to be called often. - with contextlib.suppress(AttributeError): - delattr(self, prop) + self.__dict__.pop(prop, None) @callback def _async_reconfigure(self) -> None: diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 39df2b7ce51..79e302ace74 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -26,12 +27,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that # its consistent with homeassistant.components.homekit. DIRECTION_TO_HK = { diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index fecba147a71..cbfcfb6d3bb 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -26,12 +27,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - HK_MODE_TO_HA = { 0: "off", 1: MODE_AUTO, diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index b314ffe85de..d5f20723ff1 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -24,11 +25,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 65f98ed8f5e..9fa4782e061 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -192,7 +192,7 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): ) -ENTITY_TYPES: dict[str, type[HomeKitSwitch] | type[HomeKitValve]] = { +ENTITY_TYPES: dict[str, type[HomeKitSwitch | HomeKitValve]] = { ServicesTypes.SWITCH: HomeKitSwitch, ServicesTypes.OUTLET: HomeKitSwitch, ServicesTypes.VALVE: HomeKitValve, diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 40879a533f4..2f94f5bac92 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -77,9 +77,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) await controller.async_start() diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index b0eb2a9edfa..dd89efed1c9 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -13,6 +13,7 @@ from homematicip.aio.group import AsyncHeatingGroup from homematicip.base.enums import AbsenceType from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome +from homematicip.group import HeatingCoolingProfile from homeassistant.components.climate import ( PRESET_AWAY, @@ -35,6 +36,14 @@ from .hap import HomematicipHAP HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} +NICE_PROFILE_NAMES = { + "PROFILE_1": "Default", + "PROFILE_2": "Alternative 1", + "PROFILE_3": "Alternative 2", + "PROFILE_4": "Cooling 1", + "PROFILE_5": "Cooling 2", + "PROFILE_6": "Cooling 3", +} ATTR_PRESET_END_TIME = "preset_end_time" PERMANENT_END_TIME = "permanent" @@ -164,8 +173,9 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return PRESET_ECO return ( - self._device.activeProfile.name - if self._device.activeProfile.name in self._device_profile_names + self._get_qualified_profile_name(self._device.activeProfile) + if self._get_qualified_profile_name(self._device.activeProfile) + in self._device_profile_names else None ) @@ -218,9 +228,6 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in self.preset_modes: - return - if self._device.boostMode and preset_mode != PRESET_BOOST: await self._device.set_boost(False) if preset_mode == PRESET_BOOST: @@ -256,20 +263,30 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return self._home.get_functionalHome(IndoorClimateHome) @property - def _device_profiles(self) -> list[Any]: + def _device_profiles(self) -> list[HeatingCoolingProfile]: """Return the relevant profiles.""" return [ profile for profile in self._device.profiles - if profile.visible - and profile.name != "" - and profile.index in self._relevant_profile_group + if profile.visible and profile.index in self._relevant_profile_group ] @property def _device_profile_names(self) -> list[str]: """Return a collection of profile names.""" - return [profile.name for profile in self._device_profiles] + return [ + self._get_qualified_profile_name(profile) + for profile in self._device_profiles + ] + + def _get_qualified_profile_name(self, profile: HeatingCoolingProfile) -> str: + """Get a name for the given profile. If exists, this is the name of the profile.""" + if profile.name != "": + return profile.name + if profile.index in NICE_PROFILE_NAMES: + return NICE_PROFILE_NAMES[profile.index] + + return profile.index def _get_profile_idx_by_name(self, profile_name: str) -> int: """Return a profile index by name.""" @@ -277,7 +294,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): index_name = [ profile.index for profile in self._device_profiles - if profile.name == profile_name + if self._get_qualified_profile_name(profile) == profile_name ] return relevant_index[index_name[0]] diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index c75649e6886..a2e6f8a145f 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -176,7 +176,9 @@ class HomematicipGenericEntity(Entity): """Handle hmip device removal.""" # Set marker showing that the HmIP device hase been removed. self.hmip_device_removed = True - self.hass.async_create_task(self.async_remove(force_remove=True)) + self.hass.async_create_task( + self.async_remove(force_remove=True), eager_start=False + ) @property def name(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 058b7ec6c00..7825999900e 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -39,9 +39,9 @@ class HomematicipAuth: self.auth = await self.get_auth( self.hass, self.config.get(HMIPC_HAPID), self.config.get(HMIPC_PIN) ) - return self.auth is not None except HmipcConnectionError: return False + return self.auth is not None async def async_checkbutton(self) -> bool: """Check blue butten has been pressed.""" @@ -55,9 +55,9 @@ class HomematicipAuth: try: authtoken = await self.auth.requestAuthToken() await self.auth.confirmAuthToken(authtoken) - return authtoken except HmipConnectionError: return False + return authtoken async def get_auth(self, hass: HomeAssistant, hapid, pin): """Create a HomematicIP access point object.""" diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 580a0f637c1..9da4e1bee05 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "homematicip_cloud", "name": "HomematicIP Cloud", - "codeowners": [], + "codeowners": ["@hahn-th"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 70ef47a4f03..06dbb9c8333 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -201,7 +201,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): ) from ex except Exception as ex: - _LOGGER.exception(ex) + _LOGGER.exception("Unexpected exception") raise AbortFlow("unknown_error") from ex finally: diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 83ae12dffba..fc787d98eea 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +import asyncio +from collections.abc import Mapping from dataclasses import dataclass import logging from typing import Any @@ -18,8 +20,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -40,6 +42,8 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] +CONF_COMMAND = "command" + EVENT_BUTTON_PRESS = "homeworks_button_press" EVENT_BUTTON_RELEASE = "homeworks_button_release" @@ -77,6 +81,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONTROLLER_ID): str, + vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [str]), + } +) + @dataclass class HomeworksData: @@ -87,6 +98,64 @@ class HomeworksData: keypads: dict[str, HomeworksKeypad] +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Set up services for Lutron Homeworks Series 4 and 8 integration.""" + + async def async_call_service(service_call: ServiceCall) -> None: + """Call the service.""" + await async_send_command(hass, service_call.data) + + hass.services.async_register( + DOMAIN, + "send_command", + async_call_service, + schema=SERVICE_SEND_COMMAND_SCHEMA, + ) + + +async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> None: + """Send command to a controller.""" + + def get_controller_ids() -> list[str]: + """Get homeworks data for the specified controller ID.""" + return [data.controller_id for data in hass.data[DOMAIN].values()] + + def get_homeworks_data(controller_id: str) -> HomeworksData | None: + """Get homeworks data for the specified controller ID.""" + data: HomeworksData + for data in hass.data[DOMAIN].values(): + if data.controller_id == controller_id: + return data + return None + + homeworks_data = get_homeworks_data(data[CONF_CONTROLLER_ID]) + if not homeworks_data: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_controller_id", + translation_placeholders={ + "controller_id": data[CONF_CONTROLLER_ID], + "controller_ids": ",".join(get_controller_ids()), + }, + ) + + commands = data[CONF_COMMAND] + _LOGGER.debug("Send commands: %s", commands) + for command in commands: + if command.lower().startswith("delay"): + delay = int(command.partition(" ")[2]) + _LOGGER.debug("Sleeping for %s ms", delay) + await asyncio.sleep(delay / 1000) + else: + _LOGGER.debug("Sending command '%s'", command) + await hass.async_add_executor_job( + # pylint: disable-next=protected-access + homeworks_data.controller._send, + command, + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start Homeworks controller.""" @@ -97,6 +166,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) + async_setup_services(hass) + return True diff --git a/homeassistant/components/homeworks/binary_sensor.py b/homeassistant/components/homeworks/binary_sensor.py index 9773411d26d..9a9f7086ba5 100644 --- a/homeassistant/components/homeworks/binary_sensor.py +++ b/homeassistant/components/homeworks/binary_sensor.py @@ -36,12 +36,12 @@ async def async_setup_entry( data: HomeworksData = hass.data[DOMAIN][entry.entry_id] controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] - devs = [] + entities = [] for keypad in entry.options.get(CONF_KEYPADS, []): for button in keypad[CONF_BUTTONS]: if not button[CONF_LED]: continue - dev = HomeworksBinarySensor( + entity = HomeworksBinarySensor( controller, data.keypads[keypad[CONF_ADDR]], controller_id, @@ -50,8 +50,8 @@ async def async_setup_entry( button[CONF_NAME], button[CONF_NUMBER], ) - devs.append(dev) - async_add_entities(devs, True) + entities.append(entity) + async_add_entities(entities, True) class HomeworksBinarySensor(HomeworksEntity, BinarySensorEntity): diff --git a/homeassistant/components/homeworks/button.py b/homeassistant/components/homeworks/button.py index c8cb616d95b..2f3ba482717 100644 --- a/homeassistant/components/homeworks/button.py +++ b/homeassistant/components/homeworks/button.py @@ -2,7 +2,7 @@ from __future__ import annotations -from time import sleep +import asyncio from pyhomeworks.pyhomeworks import Homeworks @@ -32,10 +32,10 @@ async def async_setup_entry( data: HomeworksData = hass.data[DOMAIN][entry.entry_id] controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] - devs = [] + entities = [] for keypad in entry.options.get(CONF_KEYPADS, []): for button in keypad[CONF_BUTTONS]: - dev = HomeworksButton( + entity = HomeworksButton( controller, controller_id, keypad[CONF_ADDR], @@ -44,8 +44,8 @@ async def async_setup_entry( button[CONF_NUMBER], button[CONF_RELEASE_DELAY], ) - devs.append(dev) - async_add_entities(devs, True) + entities.append(entity) + async_add_entities(entities, True) class HomeworksButton(HomeworksEntity, ButtonEntity): @@ -68,12 +68,19 @@ class HomeworksButton(HomeworksEntity, ButtonEntity): ) self._release_delay = release_delay - def press(self) -> None: + async def async_press(self) -> None: """Press the button.""" - # pylint: disable-next=protected-access - self._controller._send(f"KBP, {self._addr}, {self._idx}") + await self.hass.async_add_executor_job( + # pylint: disable-next=protected-access + self._controller._send, + f"KBP, {self._addr}, {self._idx}", + ) if not self._release_delay: return - sleep(self._release_delay) + await asyncio.sleep(self._release_delay) # pylint: disable-next=protected-access - self._controller._send(f"KBR, {self._addr}, {self._idx}") + await self.hass.async_add_executor_job( + # 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 index b9515c306d6..f447860c53f 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -690,7 +690,10 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PORT: user_input[CONF_PORT], } return self.async_update_reload_and_abort( - entry, options=new_options, reason="reconfigure_successful" + entry, + options=new_options, + reason="reconfigure_successful", + reload_even_if_entry_is_unchanged=False, ) return self.async_show_form( diff --git a/homeassistant/components/homeworks/icons.json b/homeassistant/components/homeworks/icons.json new file mode 100644 index 00000000000..f53b447d96e --- /dev/null +++ b/homeassistant/components/homeworks/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send_command": "mdi:console" + } +} diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 3e3c199c75c..20ae08017d3 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -28,17 +28,17 @@ async def async_setup_entry( data: HomeworksData = hass.data[DOMAIN][entry.entry_id] controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] - devs = [] + entities = [] for dimmer in entry.options.get(CONF_DIMMERS, []): - dev = HomeworksLight( + entity = HomeworksLight( controller, controller_id, dimmer[CONF_ADDR], dimmer[CONF_NAME], dimmer[CONF_RATE], ) - devs.append(dev) - async_add_entities(devs, True) + entities.append(entity) + async_add_entities(entities, True) class HomeworksLight(HomeworksEntity, LightEntity): diff --git a/homeassistant/components/homeworks/services.yaml b/homeassistant/components/homeworks/services.yaml new file mode 100644 index 00000000000..8989fc51f1d --- /dev/null +++ b/homeassistant/components/homeworks/services.yaml @@ -0,0 +1,13 @@ +send_command: + fields: + controller_id: + required: true + example: "lutron_homeworks" + selector: + text: + command: + required: true + example: "KBP, [02:08:02:01], 1" + selector: + text: + multiple: true diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json index 03c09e12888..b0d0f6e61e1 100644 --- a/homeassistant/components/homeworks/strings.json +++ b/homeassistant/components/homeworks/strings.json @@ -39,6 +39,11 @@ } } }, + "exceptions": { + "invalid_controller_id": { + "message": "Invalid controller_id \"{controller_id}\", expected one of \"{controller_ids}\"" + } + }, "options": { "error": { "duplicated_addr": "The specified address is already in use", @@ -142,5 +147,21 @@ "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_light%]" } } + }, + "services": { + "send_command": { + "name": "Send command", + "description": "Send custom command to a controller", + "fields": { + "command": { + "name": "Command", + "description": "Command to send to the controller. This can either be a single command or a list of commands." + }, + "controller_id": { + "name": "Controller ID", + "description": "The controller to which to send command." + } + } + } } } diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c1c46e2b7af..8349c383e9f 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -21,7 +21,7 @@ from .const import ( ) UPDATE_LOOP_SLEEP_TIME = 5 -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 5ac5e8a2472..ff63d66230d 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -34,8 +34,8 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -169,6 +169,7 @@ class HoneywellUSThermostat(ClimateEntity): manufacturer="Honeywell", ) + self._attr_translation_placeholders = {"name": device.name} self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT if device.temperature_unit == "C": self._attr_temperature_unit = UnitOfTemperature.CELSIUS @@ -360,15 +361,18 @@ class HoneywellUSThermostat(ClimateEntity): if mode in ["heat", "emheat"]: await self._device.set_setpoint_heat(temperature) - except UnexpectedResponse as err: + except (AscConnectionError, UnexpectedResponse) as err: raise HomeAssistantError( - "Honeywell set temperature failed: Invalid Response" + translation_domain=DOMAIN, + translation_key="temp_failed", ) from err except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) - raise ValueError( - f"Honeywell set temperature failed: invalid temperature {temperature}." + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_failed_value", + translation_placeholders={"temp": temperature}, ) from err async def async_set_temperature(self, **kwargs: Any) -> None: @@ -381,30 +385,41 @@ class HoneywellUSThermostat(ClimateEntity): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): await self._device.set_setpoint_heat(temperature) - except UnexpectedResponse as err: + except (AscConnectionError, UnexpectedResponse) as err: raise HomeAssistantError( - "Honeywell set temperature failed: Invalid Response" + translation_domain=DOMAIN, + translation_key="temp_failed", ) from err except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) - raise ValueError( - f"Honeywell set temperature failed: invalid temperature: {temperature}." + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_failed_value", + translation_placeholders={"temp": str(temperature)}, ) from err async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" try: await self._device.set_fan_mode(self._fan_mode_map[fan_mode]) + except SomeComfortError as err: - raise HomeAssistantError("Honeywell could not set fan mode.") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="fan_mode_failed", + ) from err async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" try: await self._device.set_system_mode(self._hvac_mode_map[hvac_mode]) + except SomeComfortError as err: - raise HomeAssistantError("Honeywell could not set system mode.") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="sys_mode_failed", + ) from err async def _turn_away_mode_on(self) -> None: """Turn away on. @@ -424,6 +439,12 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HEATING_MODES: await self._device.set_hold_heat(True, self._heat_away_temp) + except (AscConnectionError, UnexpectedResponse) as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="away_mode_failed", + ) from err + except SomeComfortError as err: _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", @@ -431,8 +452,14 @@ class HoneywellUSThermostat(ClimateEntity): self._heat_away_temp, self._cool_away_temp, ) - raise ValueError( - f"Honeywell set temperature failed: temperature out of range. Mode: {mode}, Heat Temperuature: {self._heat_away_temp}, Cool Temperature: {self._cool_away_temp}." + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_failed_range", + translation_placeholders={ + "heat": str(self._heat_away_temp), + "cool": str(self._cool_away_temp), + "mode": mode, + }, ) from err async def _turn_hold_mode_on(self) -> None: @@ -451,11 +478,16 @@ class HoneywellUSThermostat(ClimateEntity): except SomeComfortError as err: _LOGGER.error("Couldn't set permanent hold") raise HomeAssistantError( - "Honeywell couldn't set permanent hold." + translation_domain=DOMAIN, + translation_key="set_hold_failed", ) from err else: _LOGGER.error("Invalid system mode returned: %s", mode) - raise HomeAssistantError(f"Honeywell invalid system mode returned {mode}.") + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_mode_failed", + translation_placeholders={"mode": mode}, + ) async def _turn_away_mode_off(self) -> None: """Turn away/hold off.""" @@ -464,9 +496,13 @@ class HoneywellUSThermostat(ClimateEntity): # Disabling all hold modes await self._device.set_hold_cool(False) await self._device.set_hold_heat(False) + except SomeComfortError as err: _LOGGER.error("Can not stop hold mode") - raise HomeAssistantError("Honeywell could not stop hold mode") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="stop_hold_failed", + ) from err async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -480,22 +516,50 @@ class HoneywellUSThermostat(ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) try: await self._device.set_system_mode("emheat") + except SomeComfortError as err: raise HomeAssistantError( - "Honeywell could not set system mode to aux heat." + translation_domain=DOMAIN, + translation_key="set_aux_failed", ) from err async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" + + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) + try: if HVACMode.HEAT in self.hvac_modes: await self.async_set_hvac_mode(HVACMode.HEAT) else: await self.async_set_hvac_mode(HVACMode.OFF) + except HomeAssistantError as err: - raise HomeAssistantError("Honeywell could turn off aux heat mode.") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="disable_aux_failed", + ) from err async def async_update(self) -> None: """Get the latest state from the service.""" diff --git a/homeassistant/components/honeywell/diagnostics.py b/homeassistant/components/honeywell/diagnostics.py index b489eb4a596..35624c8fc39 100644 --- a/homeassistant/components/honeywell/diagnostics.py +++ b/homeassistant/components/honeywell/diagnostics.py @@ -16,19 +16,13 @@ async def async_get_config_entry_diagnostics( config_entry: ConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" + honeywell: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] - Honeywell: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = {} - - for device, module in Honeywell.devices.items(): - diagnostics_data.update( - { - f"Device {device}": { - "UI Data": module.raw_ui_data, - "Fan Data": module.raw_fan_data, - "DR Data": module.raw_dr_data, - } - } - ) - - return diagnostics_data + return { + f"Device {device}": { + "UI Data": module.raw_ui_data, + "Fan Data": module.raw_fan_data, + "DR Data": module.raw_dr_data, + } + for device, module in honeywell.devices.items() + } diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 6f855828e01..d3bc1924e28 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -41,6 +41,11 @@ "name": "Outdoor humidity" } }, + "switch": { + "emergency_heat": { + "name": "Emergency heat" + } + }, "climate": { "honeywell": { "state_attributes": { @@ -54,5 +59,59 @@ } } } + }, + "exceptions": { + "temp_failed": { + "message": "Honeywell set temperature failed" + }, + "sys_mode_failed": { + "message": "Honeywell could not set system mode" + }, + "fan_mode_failed": { + "message": "Honeywell could not set fan mode" + }, + "away_mode_failed": { + "message": "Honeywell set away mode failed" + }, + "temp_failed_value": { + "message": "Honeywell set temperature failed: invalid temperature {temperature}" + }, + "temp_failed_range": { + "message": "Honeywell set temperature failed: temperature out of range. Mode: {mode}, Heat Temperuature: {heat}, Cool Temperature: {cool}" + }, + "set_hold_failed": { + "message": "Honeywell could not set permanent hold" + }, + "set_mode_failed": { + "message": "Honeywell invalid system mode returned {mode}" + }, + "stop_hold_failed": { + "message": "Honeywell could not stop hold mode" + }, + "set_aux_failed": { + "message": "Honeywell could not set system mode to aux heat" + }, + "disable_aux_failed": { + "message": "Honeywell could turn off aux heat mode" + }, + "switch_failed_off": { + "message": "Honeywell could turn off emergency heat mode." + }, + "switch_failed_on": { + "message": "Honeywell could not set system mode to emergency heat mode." + } + }, + "issues": { + "service_deprecation": { + "title": "Honeywell aux heat is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::honeywell::issues::service_deprecation::title%]", + "description": "Use `switch.{name}_emergency_heat` instead to change mode.\n\nPlease adjust your automations and scripts and select **submit** to fix this issue." + } + } + } + } } } diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py new file mode 100644 index 00000000000..4aebde76727 --- /dev/null +++ b/homeassistant/components/honeywell/switch.py @@ -0,0 +1,97 @@ +"""Support for Honeywell switches.""" + +from __future__ import annotations + +from typing import Any + +from aiosomecomfort import SomeComfortError +from aiosomecomfort.device import Device as SomeComfortDevice + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HoneywellData +from .const import DOMAIN + +EMERGENCY_HEAT_KEY = "emergency_heat" + +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key=EMERGENCY_HEAT_KEY, + translation_key=EMERGENCY_HEAT_KEY, + device_class=SwitchDeviceClass.SWITCH, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Honeywell switches.""" + data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + HoneywellSwitch(hass, config_entry, device, description) + for device in data.devices.values() + if device.raw_ui_data.get("SwitchEmergencyHeatAllowed") + for description in SWITCH_TYPES + ) + + +class HoneywellSwitch(SwitchEntity): + """Representation of a honeywell switch.""" + + _attr_has_entity_name = True + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + device: SomeComfortDevice, + description: SwitchEntityDescription, + ) -> None: + """Initialize the switch.""" + self._data = hass.data[DOMAIN][config_entry.entry_id] + self._device = device + self.entity_description = description + self._attr_unique_id = f"{device.deviceid}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.deviceid)}, + name=device.name, + manufacturer="Honeywell", + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on if heat mode is enabled.""" + if self._device.system_mode == "heat": + try: + await self._device.set_system_mode("emheat") + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_on" + ) from err + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off if on.""" + if self.is_on: + try: + await self._device.set_system_mode("off") + + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_off" + ) from err + + @property + def is_on(self) -> bool: + """Return true if Emergency heat is enabled.""" + return self._device.system_mode == "emheat" diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 782340dffa6..6049f8e2434 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -165,7 +165,7 @@ HTML5_SHOWNOTIFICATION_PARAMETERS = ( ) -def get_service( +async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, @@ -173,7 +173,7 @@ def get_service( """Get the HTML5 push notification service.""" json_path = hass.config.path(REGISTRATIONS_FILE) - registrations = _load_config(json_path) + registrations = await hass.async_add_executor_job(_load_config, json_path) vapid_pub_key = config[ATTR_VAPID_PUB_KEY] vapid_prv_key = config[ATTR_VAPID_PRV_KEY] diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c9f8c21e0a3..c783d2f0b71 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -7,9 +7,11 @@ import datetime from ipaddress import IPv4Network, IPv6Network, ip_network import logging import os +import socket import ssl from tempfile import NamedTemporaryFile from typing import Any, Final, TypedDict, cast +from urllib.parse import quote_plus, urljoin from aiohttp import web from aiohttp.abc import AbstractStreamWriter @@ -19,7 +21,7 @@ from aiohttp.typedefs import JSONDecoder, StrOrURL from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection from aiohttp.web_protocol import RequestHandler from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher -from aiohttp_zlib_ng import enable_zlib_ng +from aiohttp_isal import enable_isal from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -29,8 +31,19 @@ from yarl import URL from homeassistant.components.network import async_get_source_ip from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT -from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import ( + Event, + HomeAssistant, + ServiceCall, + ServiceResponse, + callback, +) +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceValidationError, + Unauthorized, + UnknownUser, +) from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.http import ( @@ -52,9 +65,14 @@ 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 .auth import async_setup_auth, async_sign_path from .ban import setup_bans -from .const import KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER # noqa: F401 +from .const import ( # noqa: F401 + DOMAIN, + KEY_HASS_REFRESH_TOKEN_ID, + KEY_HASS_USER, + StrictConnectionMode, +) from .cors import setup_cors from .decorators import require_admin # noqa: F401 from .forwarded import async_setup_forwarded @@ -64,8 +82,6 @@ from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource from .web_runner import HomeAssistantTCPSite -DOMAIN: Final = "http" - CONF_SERVER_HOST: Final = "server_host" CONF_SERVER_PORT: Final = "server_port" CONF_BASE_URL: Final = "base_url" @@ -79,6 +95,7 @@ CONF_TRUSTED_PROXIES: Final = "trusted_proxies" CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold" CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled" CONF_SSL_PROFILE: Final = "ssl_profile" +CONF_STRICT_CONNECTION: Final = "strict_connection" SSL_MODERN: Final = "modern" SSL_INTERMEDIATE: Final = "intermediate" @@ -98,11 +115,14 @@ STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 SAVE_DELAY: Final = 180 +_HAS_IPV6 = hasattr(socket, "AF_INET6") +_DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"] + HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { - vol.Optional(CONF_SERVER_HOST): vol.All( + vol.Optional(CONF_SERVER_HOST, default=_DEFAULT_BIND): vol.All( cv.ensure_list, vol.Length(min=1), [cv.string] ), vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, @@ -176,14 +196,14 @@ class ApiConfig: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HTTP API and debug interface.""" - enable_zlib_ng() + enable_isal() conf: ConfData | None = config.get(DOMAIN) if conf is None: conf = cast(ConfData, HTTP_SCHEMA({})) - server_host = conf.get(CONF_SERVER_HOST) + server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) @@ -214,6 +234,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: login_threshold=login_threshold, is_ban_enabled=is_ban_enabled, use_x_frame_options=use_x_frame_options, + strict_connection_non_cloud=StrictConnectionMode.DISABLED, ) async def stop_server(event: Event) -> None: @@ -243,6 +264,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: local_ip, host, server_port, ssl_certificate is not None ) + _setup_services(hass, conf) return True @@ -327,6 +349,7 @@ class HomeAssistantHTTP: login_threshold: int, is_ban_enabled: bool, use_x_frame_options: bool, + strict_connection_non_cloud: StrictConnectionMode, ) -> None: """Initialize the server.""" self.app[KEY_HASS] = self.hass @@ -343,7 +366,7 @@ class HomeAssistantHTTP: if is_ban_enabled: setup_bans(self.hass, self.app, login_threshold) - await async_setup_auth(self.hass, self.app) + await async_setup_auth(self.hass, self.app, strict_connection_non_cloud) setup_headers(self.app, use_x_frame_options) setup_cors(self.app, cors_origins) @@ -573,3 +596,54 @@ async def start_http_server_and_save_config( ] store.async_delay_save(lambda: conf, SAVE_DELAY) + + +@callback +def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: + """Set up services for HTTP component.""" + + async def create_temporary_strict_connection_url( + call: ServiceCall, + ) -> ServiceResponse: + """Create a strict connection url and return it.""" + # Copied form homeassistant/helpers/service.py#_async_admin_handler + # as the helper supports no responses yet + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + if user is None: + raise UnknownUser(context=call.context) + if not user.is_admin: + raise Unauthorized(context=call.context) + + if StrictConnectionMode.DISABLED is StrictConnectionMode.DISABLED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="strict_connection_not_enabled_non_cloud", + ) + + try: + url = get_url( + hass, prefer_external=True, allow_internal=False, allow_cloud=False + ) + except NoURLAvailableError as ex: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_external_url_available", + ) from ex + + # to avoid circular import + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.auth import STRICT_CONNECTION_URL + + path = async_sign_path( + hass, + STRICT_CONNECTION_URL, + datetime.timedelta(hours=1), + use_content_user=True, + ) + url = urljoin(url, path) + + return { + "url": f"https://login.home-assistant.io?u={quote_plus(url)}", + "direct_url": url, + } diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 2073c998384..58dae21d2a6 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -4,14 +4,18 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import timedelta +from http import HTTPStatus from ipaddress import ip_address import logging +import os import secrets import time from typing import Any, Final from aiohttp import hdrs -from aiohttp.web import Application, Request, StreamResponse, middleware +from aiohttp.web import Application, Request, Response, StreamResponse, middleware +from aiohttp.web_exceptions import HTTPBadRequest +from aiohttp_session import session_middleware import jwt from jwt import api_jws from yarl import URL @@ -21,13 +25,21 @@ from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import singleton from homeassistant.helpers.http import current_request from homeassistant.helpers.json import json_bytes from homeassistant.helpers.network import is_cloud_connection from homeassistant.helpers.storage import Store from homeassistant.util.network import is_local -from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER +from .const import ( + DOMAIN, + KEY_AUTHENTICATED, + KEY_HASS_REFRESH_TOKEN_ID, + KEY_HASS_USER, + StrictConnectionMode, +) +from .session import HomeAssistantCookieStorage _LOGGER = logging.getLogger(__name__) @@ -39,6 +51,11 @@ SAFE_QUERY_PARAMS: Final = ["height", "width"] STORAGE_VERSION = 1 STORAGE_KEY = "http.auth" CONTENT_USER_NAME = "Home Assistant Content" +STRICT_CONNECTION_EXCLUDED_PATH = "/api/webhook/" +STRICT_CONNECTION_GUARD_PAGE_NAME = "strict_connection_guard_page.html" +STRICT_CONNECTION_GUARD_PAGE = os.path.join( + os.path.dirname(__file__), STRICT_CONNECTION_GUARD_PAGE_NAME +) @callback @@ -48,13 +65,16 @@ def async_sign_path( expiration: timedelta, *, refresh_token_id: str | None = None, + use_content_user: bool = False, ) -> str: """Sign a path for temporary access without auth header.""" if (secret := hass.data.get(DATA_SIGN_SECRET)) is None: secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() if refresh_token_id is None: - if connection := websocket_api.current_connection.get(): + if use_content_user: + refresh_token_id = hass.data[STORAGE_KEY] + elif connection := websocket_api.current_connection.get(): refresh_token_id = connection.refresh_token_id elif ( request := current_request.get() @@ -114,7 +134,11 @@ def async_user_not_allowed_do_auth( return "User cannot authenticate remotely" -async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: +async def async_setup_auth( + hass: HomeAssistant, + app: Application, + strict_connection_mode_non_cloud: StrictConnectionMode, +) -> None: """Create auth middleware for the app.""" store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) if (data := await store.async_load()) is None: @@ -136,6 +160,10 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: hass.data[STORAGE_KEY] = refresh_token.id + if strict_connection_mode_non_cloud is StrictConnectionMode.GUARD_PAGE: + # Load the guard page content on setup + await _read_strict_connection_guard_page(hass) + @callback def async_validate_auth_header(request: Request) -> bool: """Test authorization header against access token. @@ -224,6 +252,37 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: authenticated = True auth_type = "signed request" + if not authenticated and not request.path.startswith( + STRICT_CONNECTION_EXCLUDED_PATH + ): + strict_connection_mode = strict_connection_mode_non_cloud + strict_connection_func = ( + _async_perform_strict_connection_action_on_non_local + ) + if is_cloud_connection(hass): + from homeassistant.components.cloud.util import ( # pylint: disable=import-outside-toplevel + get_strict_connection_mode, + ) + + strict_connection_mode = get_strict_connection_mode(hass) + strict_connection_func = _async_perform_strict_connection_action + + if ( + strict_connection_mode is not StrictConnectionMode.DISABLED + and not await hass.auth.session.async_validate_request_for_strict_connection_session( + request + ) + and ( + resp := await strict_connection_func( + hass, + request, + strict_connection_mode is StrictConnectionMode.GUARD_PAGE, + ) + ) + is not None + ): + return resp + if authenticated and _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Authenticated %s for %s using %s", @@ -235,4 +294,69 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: request[KEY_AUTHENTICATED] = authenticated return await handler(request) + app.middlewares.append(session_middleware(HomeAssistantCookieStorage(hass))) app.middlewares.append(auth_middleware) + + +async def _async_perform_strict_connection_action_on_non_local( + hass: HomeAssistant, + request: Request, + guard_page: bool, +) -> StreamResponse | None: + """Perform strict connection mode action if the request is not local. + + The function does the following: + - Try to get the IP address of the request. If it fails, assume it's not local + - If the request is local, return None (allow the request to continue) + - If guard_page is True, return a response with the content + - Otherwise close the connection and raise an exception + """ + try: + ip_address_ = ip_address(request.remote) # type: ignore[arg-type] + except ValueError: + _LOGGER.debug("Invalid IP address: %s", request.remote) + ip_address_ = None + + if ip_address_ and is_local(ip_address_): + return None + + return await _async_perform_strict_connection_action(hass, request, guard_page) + + +async def _async_perform_strict_connection_action( + hass: HomeAssistant, + request: Request, + guard_page: bool, +) -> StreamResponse | None: + """Perform strict connection mode action. + + The function does the following: + - If guard_page is True, return a response with the content + - Otherwise close the connection and raise an exception + """ + + _LOGGER.debug("Perform strict connection action for %s", request.remote) + if guard_page: + return Response( + text=await _read_strict_connection_guard_page(hass), + content_type="text/html", + status=HTTPStatus.IM_A_TEAPOT, + ) + + if transport := request.transport: + # it should never happen that we don't have a transport + transport.close() + + # We need to raise an exception to stop processing the request + raise HTTPBadRequest + + +@singleton.singleton(f"{DOMAIN}_{STRICT_CONNECTION_GUARD_PAGE_NAME}") +async def _read_strict_connection_guard_page(hass: HomeAssistant) -> str: + """Read the strict connection guard page from disk via executor.""" + + def read_guard_page() -> str: + with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file: + return file.read() + + return await hass.async_add_executor_job(read_guard_page) diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 1254744f258..4a15e310b11 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -1,8 +1,19 @@ """HTTP specific constants.""" +from enum import StrEnum from typing import Final from homeassistant.helpers.http import KEY_AUTHENTICATED, KEY_HASS # noqa: F401 +DOMAIN: Final = "http" + KEY_HASS_USER: Final = "hass_user" KEY_HASS_REFRESH_TOKEN_ID: Final = "hass_refresh_token_id" + + +class StrictConnectionMode(StrEnum): + """Enum for strict connection mode.""" + + DISABLED = "disabled" + GUARD_PAGE = "guard_page" + DROP_CONNECTION = "drop_connection" diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index ebae2480589..d97ac9922a2 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -13,6 +13,7 @@ from aiohttp.web_urldispatcher import ( ResourceRoute, StaticResource, ) +import aiohttp_cors from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback @@ -35,11 +36,6 @@ VALID_CORS_TYPES: Final = (Resource, ResourceRoute, StaticResource) @callback def setup_cors(app: Application, origins: list[str]) -> None: """Set up CORS.""" - # This import should remain here. That way the HTTP integration can always - # be imported by other integrations without it's requirements being installed. - # pylint: disable-next=import-outside-toplevel - import aiohttp_cors - cors = aiohttp_cors.setup( app, defaults={ diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 749c4f63a2f..e1ba1caae56 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -70,7 +70,6 @@ class RequestDataValidator: f"Message format incorrect: {err}", HTTPStatus.BAD_REQUEST ) - result = await method(view, request, data, *args, **kwargs) - return result + return await method(view, request, data, *args, **kwargs) return wrapper diff --git a/homeassistant/components/http/headers.py b/homeassistant/components/http/headers.py index bd05401ebce..3c845601183 100644 --- a/homeassistant/components/http/headers.py +++ b/homeassistant/components/http/headers.py @@ -33,7 +33,7 @@ def setup_headers(app: Application, use_x_frame_options: bool) -> None: except HTTPException as err: for key, value in added_headers.items(): err.headers[key] = value - raise err + raise for key, value in added_headers.items(): response.headers[key] = value diff --git a/homeassistant/components/http/icons.json b/homeassistant/components/http/icons.json new file mode 100644 index 00000000000..8e8b6285db7 --- /dev/null +++ b/homeassistant/components/http/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "create_temporary_strict_connection_url": "mdi:login-variant" + } +} diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 647b7e42a3a..fb804251edc 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -5,10 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/http", "integration_type": "system", "iot_class": "local_push", - "quality_scale": "internal", - "requirements": [ - "aiohttp_cors==0.7.0", - "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-zlib-ng==0.3.1" - ] + "quality_scale": "internal" } diff --git a/homeassistant/components/http/services.yaml b/homeassistant/components/http/services.yaml new file mode 100644 index 00000000000..16b0debb144 --- /dev/null +++ b/homeassistant/components/http/services.yaml @@ -0,0 +1 @@ +create_temporary_strict_connection_url: ~ diff --git a/homeassistant/components/http/session.py b/homeassistant/components/http/session.py new file mode 100644 index 00000000000..81668ec2ccc --- /dev/null +++ b/homeassistant/components/http/session.py @@ -0,0 +1,160 @@ +"""Session http module.""" + +from functools import lru_cache +import logging + +from aiohttp.web import Request, StreamResponse +from aiohttp_session import Session, SessionData +from aiohttp_session.cookie_storage import EncryptedCookieStorage +from cryptography.fernet import InvalidToken + +from homeassistant.auth.const import REFRESH_TOKEN_EXPIRATION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import json_dumps +from homeassistant.helpers.network import is_cloud_connection +from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads + +from .ban import process_wrong_login + +_LOGGER = logging.getLogger(__name__) + +COOKIE_NAME = "SC" +PREFIXED_COOKIE_NAME = f"__Host-{COOKIE_NAME}" +SESSION_CACHE_SIZE = 16 + + +def _get_cookie_name(is_secure: bool) -> str: + """Return the cookie name.""" + return PREFIXED_COOKIE_NAME if is_secure else COOKIE_NAME + + +class HomeAssistantCookieStorage(EncryptedCookieStorage): + """Home Assistant cookie storage. + + Own class is required: + - to set the secure flag based on the connection type + - to use a LRU cache for session decryption + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the cookie storage.""" + super().__init__( + hass.auth.session.key, + cookie_name=PREFIXED_COOKIE_NAME, + max_age=int(REFRESH_TOKEN_EXPIRATION), + httponly=True, + samesite="Lax", + secure=True, + encoder=json_dumps, + decoder=json_loads, + ) + self._hass = hass + + def _secure_connection(self, request: Request) -> bool: + """Return if the connection is secure (https).""" + return is_cloud_connection(self._hass) or request.secure + + def load_cookie(self, request: Request) -> str | None: + """Load cookie.""" + is_secure = self._secure_connection(request) + cookie_name = _get_cookie_name(is_secure) + return request.cookies.get(cookie_name) + + @lru_cache(maxsize=SESSION_CACHE_SIZE) + def _decrypt_cookie(self, cookie: str) -> Session | None: + """Decrypt and validate cookie.""" + try: + data = SessionData( # type: ignore[misc] + self._decoder( + self._fernet.decrypt( + cookie.encode("utf-8"), ttl=self.max_age + ).decode("utf-8") + ) + ) + except (InvalidToken, TypeError, ValueError, *JSON_DECODE_EXCEPTIONS): + _LOGGER.warning("Cannot decrypt/parse cookie value") + return None + + session = Session(None, data=data, new=data is None, max_age=self.max_age) + + # Validate session if not empty + if ( + not session.empty + and not self._hass.auth.session.async_validate_strict_connection_session( + session + ) + ): + # Invalidate session as it is not valid + session.invalidate() + + return session + + async def new_session(self) -> Session: + """Create a new session and mark it as changed.""" + session = Session(None, data=None, new=True, max_age=self.max_age) + session.changed() + return session + + async def load_session(self, request: Request) -> Session: + """Load session.""" + # Split parent function to use lru_cache + if (cookie := self.load_cookie(request)) is None: + return await self.new_session() + + if (session := self._decrypt_cookie(cookie)) is None: + # Decrypting/parsing failed, log wrong login and create a new session + await process_wrong_login(request) + session = await self.new_session() + + return session + + async def save_session( + self, request: Request, response: StreamResponse, session: Session + ) -> None: + """Save session.""" + + is_secure = self._secure_connection(request) + cookie_name = _get_cookie_name(is_secure) + + if session.empty: + response.del_cookie(cookie_name) + else: + params = self.cookie_params.copy() + params["secure"] = is_secure + params["max_age"] = session.max_age + + cookie_data = self._encoder(self._get_session_data(session)).encode("utf-8") + response.set_cookie( + cookie_name, + self._fernet.encrypt(cookie_data).decode("utf-8"), + **params, + ) + # Add Cache-Control header to not cache the cookie as it + # is used for session management + self._add_cache_control_header(response) + + @staticmethod + def _add_cache_control_header(response: StreamResponse) -> None: + """Add/set cache control header to no-cache="Set-Cookie".""" + # Structure of the Cache-Control header defined in + # https://datatracker.ietf.org/doc/html/rfc2068#section-14.9 + if header := response.headers.get("Cache-Control"): + directives = [] + for directive in header.split(","): + directive = directive.strip() + directive_lowered = directive.lower() + if directive_lowered.startswith("no-cache"): + if "set-cookie" in directive_lowered or directive.find("=") == -1: + # Set-Cookie is already in the no-cache directive or + # the whole request should not be cached -> Nothing to do + return + + # Add Set-Cookie to the no-cache + # [:-1] to remove the " at the end of the directive + directive = f"{directive[:-1]}, Set-Cookie" + + directives.append(directive) + header = ", ".join(directives) + else: + header = 'no-cache="Set-Cookie"' + response.headers["Cache-Control"] = header diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index fd6cd742ce4..b7bb9d4f3a8 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -58,7 +58,7 @@ class CachingStaticResource(StaticResource): raise except Exception as error: # perm error or other kind! - request.app.logger.exception(error) + request.app.logger.exception("Unexpected exception") raise HTTPNotFound from error content_type: str | None = None diff --git a/homeassistant/components/http/strict_connection_guard_page.html b/homeassistant/components/http/strict_connection_guard_page.html new file mode 100644 index 00000000000..8567e500c9d --- /dev/null +++ b/homeassistant/components/http/strict_connection_guard_page.html @@ -0,0 +1,140 @@ + + + + + + Home Assistant - Access denied + + + + +
+ + + + +
+
+

You need access

+

+ This device is not known to + Home Assistant. +

+ + + Learn how to get access + +
+ + diff --git a/homeassistant/components/http/strings.json b/homeassistant/components/http/strings.json new file mode 100644 index 00000000000..7cd64f5f297 --- /dev/null +++ b/homeassistant/components/http/strings.json @@ -0,0 +1,16 @@ +{ + "exceptions": { + "strict_connection_not_enabled_non_cloud": { + "message": "Strict connection is not enabled for non-cloud requests" + }, + "no_external_url_available": { + "message": "No external URL available" + } + }, + "services": { + "create_temporary_strict_connection_url": { + "name": "Create a temporary strict connection URL", + "description": "Create a temporary strict connection URL, which can be used to login on another device." + } + } +} diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index e75fef42ef3..cef5bc5030e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -79,9 +79,9 @@ def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None: try: last_reset = datetime.now() - timedelta(seconds=int(value)) last_reset.replace(microsecond=0) - return last_reset except ValueError: return None + return last_reset def signal_icon(limits: Sequence[int], value: StateType) -> str: diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 1af294d8640..da79df6d52f 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -56,12 +56,6 @@ from .const import ( # noqa: F401 HumidifierEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index 03ab02429bb..fe6f6978014 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.LAWN_MOWER, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 2188725ed76..8d9588db5b7 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -16,6 +16,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) MAX_WS_RECONNECT_TIME = 600 +SCAN_INTERVAL = timedelta(minutes=8) class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]): @@ -29,7 +30,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(minutes=5), + update_interval=SCAN_INTERVAL, ) self.api = api diff --git a/homeassistant/components/husqvarna_automower/device_tracker.py b/homeassistant/components/husqvarna_automower/device_tracker.py index a32fd8758bd..780d1da76fb 100644 --- a/homeassistant/components/husqvarna_automower/device_tracker.py +++ b/homeassistant/components/husqvarna_automower/device_tracker.py @@ -1,5 +1,7 @@ """Creates the device tracker entity for the mower.""" +from typing import TYPE_CHECKING + from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -44,9 +46,13 @@ class AutomowerDeviceTrackerEntity(AutomowerBaseEntity, TrackerEntity): @property def latitude(self) -> float: """Return latitude value of the device.""" + if TYPE_CHECKING: + assert self.mower_attributes.positions is not None return self.mower_attributes.positions[0].latitude @property def longitude(self) -> float: """Return longitude value of the device.""" + if TYPE_CHECKING: + assert self.mower_attributes.positions is not None return self.mower_attributes.positions[0].longitude diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index 65cc85bd09b..2ecbf9c198a 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -8,17 +8,28 @@ "default": "mdi:debug-step-into" } }, + "number": { + "cutting_height": { + "default": "mdi:grass" + } + }, "select": { "headlight_mode": { "default": "mdi:car-light-high" } }, "sensor": { + "error": { + "default": "mdi:alert-circle-outline" + }, "number_of_charging_cycles": { "default": "mdi:battery-sync-outline" }, "number_of_collisions": { "default": "mdi:counter" + }, + "restricted_reason": { + "default": "mdi:tooltip-question" } } } diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index e4536ee594d..147c6dfb6d5 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.4"] + "requirements": ["aioautomower==2024.4.3"] } diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py new file mode 100644 index 00000000000..e2e617b427b --- /dev/null +++ b/homeassistant/components/husqvarna_automower/number.py @@ -0,0 +1,104 @@ +"""Creates the number entities for the mower.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +import logging +from typing import TYPE_CHECKING, Any + +from aioautomower.exceptions import ApiException +from aioautomower.model import MowerAttributes +from aioautomower.session import AutomowerSession + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +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.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 AutomowerNumberEntityDescription(NumberEntityDescription): + """Describes Automower number entity.""" + + exists_fn: Callable[[MowerAttributes], bool] = lambda _: True + value_fn: Callable[[MowerAttributes], int] + set_value_fn: Callable[[AutomowerSession, str, float], Awaitable[Any]] + + +@callback +def _async_get_cutting_height(data: MowerAttributes) -> int: + """Return the cutting height.""" + if TYPE_CHECKING: + # Sensor does not get created if it is None + assert data.cutting_height is not None + return data.cutting_height + + +NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = ( + AutomowerNumberEntityDescription( + key="cutting_height", + translation_key="cutting_height", + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + native_min_value=1, + native_max_value=9, + exists_fn=lambda data: data.cutting_height is not None, + value_fn=_async_get_cutting_height, + set_value_fn=lambda session, mower_id, cheight: session.set_cutting_height( + mower_id, int(cheight) + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up number platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerNumberEntity(mower_id, coordinator, description) + for mower_id in coordinator.data + for description in NUMBER_TYPES + if description.exists_fn(coordinator.data[mower_id]) + ) + + +class AutomowerNumberEntity(AutomowerBaseEntity, NumberEntity): + """Defining the AutomowerNumberEntity with AutomowerNumberEntityDescription.""" + + entity_description: AutomowerNumberEntityDescription + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + description: AutomowerNumberEntityDescription, + ) -> None: + """Set up AutomowerNumberEntity.""" + super().__init__(mower_id, coordinator) + self.entity_description = description + self._attr_unique_id = f"{mower_id}_{description.key}" + + @property + def native_value(self) -> float: + """Return the state of the number.""" + return self.entity_description.value_fn(self.mower_attributes) + + async def async_set_native_value(self, value: float) -> None: + """Change to new number value.""" + try: + await self.entity_description.set_value_fn( + self.coordinator.api, self.mower_id, value + ) + 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/select.py b/homeassistant/components/husqvarna_automower/select.py index e4376a1bca5..67aac4a2046 100644 --- a/homeassistant/components/husqvarna_automower/select.py +++ b/homeassistant/components/husqvarna_automower/select.py @@ -1,6 +1,7 @@ """Creates a select entity for the headlight of the mower.""" import logging +from typing import cast from aioautomower.exceptions import ApiException from aioautomower.model import HeadlightModes @@ -58,12 +59,14 @@ class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity): @property def current_option(self) -> str: """Return the current option for the entity.""" - return self.mower_attributes.headlight.mode.lower() + return cast(HeadlightModes, 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()) + await self.coordinator.api.set_headlight_mode( + self.mower_id, cast(HeadlightModes, option.upper()) + ) except ApiException as exception: raise HomeAssistantError( f"Command couldn't be sent to the command queue: {exception}" diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index e054d02e3ba..6840708ed42 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import datetime import logging -from aioautomower.model import MowerAttributes, MowerModes +from aioautomower.model import MowerAttributes, MowerModes, RestrictedReasons from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +18,6 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOf 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 @@ -26,6 +25,165 @@ from .entity import AutomowerBaseEntity _LOGGER = logging.getLogger(__name__) +ERROR_KEY_LIST = [ + "no_error", + "alarm_mower_in_motion", + "alarm_mower_lifted", + "alarm_mower_stopped", + "alarm_mower_switched_off", + "alarm_mower_tilted", + "alarm_outside_geofence", + "angular_sensor_problem", + "battery_problem", + "battery_problem", + "battery_restriction_due_to_ambient_temperature", + "can_error", + "charging_current_too_high", + "charging_station_blocked", + "charging_system_problem", + "charging_system_problem", + "collision_sensor_defect", + "collision_sensor_error", + "collision_sensor_problem_front", + "collision_sensor_problem_rear", + "com_board_not_available", + "communication_circuit_board_sw_must_be_updated", + "complex_working_area", + "connection_changed", + "connection_not_changed", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_settings_restored", + "cutting_drive_motor_1_defect", + "cutting_drive_motor_2_defect", + "cutting_drive_motor_3_defect", + "cutting_height_blocked", + "cutting_height_problem", + "cutting_height_problem_curr", + "cutting_height_problem_dir", + "cutting_height_problem_drive", + "cutting_motor_problem", + "cutting_stopped_slope_too_steep", + "cutting_system_blocked", + "cutting_system_blocked", + "cutting_system_imbalance_warning", + "cutting_system_major_imbalance", + "destination_not_reachable", + "difficult_finding_home", + "docking_sensor_defect", + "electronic_problem", + "empty_battery", + "folding_cutting_deck_sensor_defect", + "folding_sensor_activated", + "geofence_problem", + "geofence_problem", + "gps_navigation_problem", + "guide_1_not_found", + "guide_2_not_found", + "guide_3_not_found", + "guide_calibration_accomplished", + "guide_calibration_failed", + "high_charging_power_loss", + "high_internal_power_loss", + "high_internal_temperature", + "internal_voltage_error", + "invalid_battery_combination_invalid_combination_of_different_battery_types", + "invalid_sub_device_combination", + "invalid_system_configuration", + "left_brush_motor_overloaded", + "lift_sensor_defect", + "lifted", + "limited_cutting_height_range", + "limited_cutting_height_range", + "loop_sensor_defect", + "loop_sensor_problem_front", + "loop_sensor_problem_left", + "loop_sensor_problem_rear", + "loop_sensor_problem_right", + "low_battery", + "memory_circuit_problem", + "mower_lifted", + "mower_tilted", + "no_accurate_position_from_satellites", + "no_confirmed_position", + "no_drive", + "no_loop_signal", + "no_power_in_charging_station", + "no_response_from_charger", + "outside_working_area", + "poor_signal_quality", + "reference_station_communication_problem", + "right_brush_motor_overloaded", + "safety_function_faulty", + "settings_restored", + "sim_card_locked", + "sim_card_locked", + "sim_card_locked", + "sim_card_locked", + "sim_card_not_found", + "sim_card_requires_pin", + "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern", + "slope_too_steep", + "sms_could_not_be_sent", + "stop_button_problem", + "stuck_in_charging_station", + "switch_cord_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "tilt_sensor_problem", + "too_high_discharge_current", + "too_high_internal_current", + "trapped", + "ultrasonic_problem", + "ultrasonic_sensor_1_defect", + "ultrasonic_sensor_2_defect", + "ultrasonic_sensor_3_defect", + "ultrasonic_sensor_4_defect", + "unexpected_cutting_height_adj", + "unexpected_error", + "upside_down", + "weak_gps_signal", + "wheel_drive_problem_left", + "wheel_drive_problem_rear_left", + "wheel_drive_problem_rear_right", + "wheel_drive_problem_right", + "wheel_motor_blocked_left", + "wheel_motor_blocked_rear_left", + "wheel_motor_blocked_rear_right", + "wheel_motor_blocked_right", + "wheel_motor_overloaded_left", + "wheel_motor_overloaded_rear_left", + "wheel_motor_overloaded_rear_right", + "wheel_motor_overloaded_right", + "work_area_not_valid", + "wrong_loop_signal", + "wrong_pin_code", + "zone_generator_problem", +] + +RESTRICTED_REASONS: list = [ + RestrictedReasons.ALL_WORK_AREAS_COMPLETED.lower(), + RestrictedReasons.DAILY_LIMIT.lower(), + RestrictedReasons.EXTERNAL.lower(), + RestrictedReasons.FOTA.lower(), + RestrictedReasons.FROST.lower(), + RestrictedReasons.NONE.lower(), + RestrictedReasons.NOT_APPLICABLE.lower(), + RestrictedReasons.PARK_OVERRIDE.lower(), + RestrictedReasons.SENSOR.lower(), + RestrictedReasons.WEEK_SCHEDULE.lower(), +] + @dataclass(frozen=True, kw_only=True) class AutomowerSensorEntityDescription(SensorEntityDescription): @@ -139,7 +297,23 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( key="next_start_timestamp", translation_key="next_start_timestamp", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: dt_util.as_local(data.planner.next_start_datetime), + value_fn=lambda data: data.planner.next_start_datetime, + ), + AutomowerSensorEntityDescription( + key="error", + translation_key="error", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: ( + "no_error" if data.mower.error_key is None else data.mower.error_key + ), + options=ERROR_KEY_LIST, + ), + AutomowerSensorEntityDescription( + key="restricted_reason", + translation_key="restricted_reason", + device_class=SensorDeviceClass.ENUM, + options=RESTRICTED_REASONS, + value_fn=lambda data: data.planner.restricted_reason.lower(), ), ) diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 8032c670404..b4c1c97cd68 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -37,6 +37,11 @@ "name": "Returning to dock" } }, + "number": { + "cutting_height": { + "name": "Cutting height" + } + }, "select": { "headlight_mode": { "name": "Headlight mode", @@ -49,6 +54,134 @@ } }, "sensor": { + "error": { + "name": "Error", + "state": { + "alarm_mower_in_motion": "Alarm! Mower in motion", + "alarm_mower_lifted": "Alarm! Mower lifted", + "alarm_mower_stopped": "Alarm! Mower stopped", + "alarm_mower_switched_off": "Alarm! Mower switched off", + "alarm_mower_tilted": "Alarm! Mower tilted", + "alarm_outside_geofence": "Alarm! Outside geofence", + "angular_sensor_problem": "Angular sensor problem", + "battery_problem": "Battery problem", + "battery_restriction_due_to_ambient_temperature": "Battery restriction due to ambient temperature", + "can_error": "CAN error", + "charging_current_too_high": "Charging current too high", + "charging_station_blocked": "Charging station blocked", + "charging_system_problem": "Charging system problem", + "collision_sensor_defect": "Collision sensor defect", + "collision_sensor_error": "Collision sensor error", + "collision_sensor_problem_front": "Front collision sensor problem", + "collision_sensor_problem_rear": "Rear collision sensor problem", + "com_board_not_available": "Com board not available", + "communication_circuit_board_sw_must_be_updated": "Communication circuit board software must be updated", + "complex_working_area": "Complex working area", + "connection_changed": "Connection changed", + "connection_not_changed": "Connection NOT changed", + "connectivity_problem": "Connectivity problem", + "connectivity_settings_restored": "Connectivity settings restored", + "cutting_drive_motor_1_defect": "Cutting drive motor 1 defect", + "cutting_drive_motor_2_defect": "Cutting drive motor 2 defect", + "cutting_drive_motor_3_defect": "Cutting drive motor 3 defect", + "cutting_height_blocked": "Cutting height blocked", + "cutting_height_problem": "Cutting height problem", + "cutting_height_problem_curr": "Cutting height problem, curr", + "cutting_height_problem_dir": "Cutting height problem, dir", + "cutting_height_problem_drive": "Cutting height problem, drive", + "cutting_motor_problem": "Cutting motor problem", + "cutting_stopped_slope_too_steep": "Cutting stopped - slope too steep", + "cutting_system_blocked": "Cutting system blocked", + "cutting_system_imbalance_warning": "Cutting system imbalance", + "cutting_system_major_imbalance": "Cutting system major imbalance", + "destination_not_reachable": "Destination not reachable", + "difficult_finding_home": "Difficult finding home", + "docking_sensor_defect": "Docking sensor defect", + "electronic_problem": "Electronic problem", + "empty_battery": "Empty battery", + "folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect", + "folding_sensor_activated": "Folding sensor activated", + "geofence_problem": "Geofence problem", + "gps_navigation_problem": "GPS navigation problem", + "guide_1_not_found": "Guide 1 not found", + "guide_2_not_found": "Guide 2 not found", + "guide_3_not_found": "Guide 3 not found", + "guide_calibration_accomplished": "Guide calibration accomplished", + "guide_calibration_failed": "Guide calibration failed", + "high_charging_power_loss": "High charging power loss", + "high_internal_power_loss": "High internal power loss", + "high_internal_temperature": "High internal temperature", + "internal_voltage_error": "Internal voltage error", + "invalid_battery_combination_invalid_combination_of_different_battery_types": "Invalid battery combination - Invalid combination of different battery types.", + "invalid_sub_device_combination": "Invalid sub-device combination", + "invalid_system_configuration": "Invalid system configuration", + "left_brush_motor_overloaded": "Left brush motor overloaded", + "lift_sensor_defect": "Lift Sensor defect", + "lifted": "Lifted", + "limited_cutting_height_range": "Limited cutting height range", + "loop_sensor_defect": "Loop sensor defect", + "loop_sensor_problem_front": "Front loop sensor problem", + "loop_sensor_problem_left": "Left loop sensor problem", + "loop_sensor_problem_rear": "Rear loop sensor problem", + "loop_sensor_problem_right": "Right loop sensor problem", + "low_battery": "Low battery", + "memory_circuit_problem": "Memory circuit problem", + "mower_lifted": "Mower lifted", + "mower_tilted": "Mower tilted", + "no_accurate_position_from_satellites": "No accurate position from satellites", + "no_confirmed_position": "No confirmed position", + "no_drive": "No drive", + "no_error": "No error", + "no_loop_signal": "No loop signal", + "no_power_in_charging_station": "No power in charging station", + "no_response_from_charger": "No response from charger", + "outside_working_area": "Outside working area", + "poor_signal_quality": "Poor signal quality", + "reference_station_communication_problem": "Reference station communication problem", + "right_brush_motor_overloaded": "Right brush motor overloaded", + "safety_function_faulty": "Safety function faulty", + "settings_restored": "Settings restored", + "sim_card_locked": "SIM card locked", + "sim_card_not_found": "SIM card not found", + "sim_card_requires_pin": "SIM card requires PIN", + "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern": "Slipped - Mower has Slipped. Situation not solved with moving pattern", + "slope_too_steep": "Slope too steep", + "sms_could_not_be_sent": "SMS could not be sent", + "stop_button_problem": "STOP button problem", + "stuck_in_charging_station": "Stuck in charging station", + "switch_cord_problem": "Switch cord problem", + "temporary_battery_problem": "Temporary battery problem", + "tilt_sensor_problem": "Tilt sensor problem", + "too_high_discharge_current": "Discharge current too high", + "too_high_internal_current": "Internal current too high", + "trapped": "Trapped", + "ultrasonic_problem": "Ultrasonic problem", + "ultrasonic_sensor_1_defect": "Ultrasonic Sensor 1 defect", + "ultrasonic_sensor_2_defect": "Ultrasonic Sensor 2 defect", + "ultrasonic_sensor_3_defect": "Ultrasonic Sensor 3 defect", + "ultrasonic_sensor_4_defect": "Ultrasonic Sensor 4 defect", + "unexpected_cutting_height_adj": "Unexpected cutting height adjustment", + "unexpected_error": "Unexpected error", + "upside_down": "Upside down", + "weak_gps_signal": "Weak GPS signal", + "wheel_drive_problem_left": "Left wheel drive problem", + "wheel_drive_problem_rear_left": "Rear left wheel drive problem", + "wheel_drive_problem_rear_right": "Rear right wheel drive problem", + "wheel_drive_problem_right": "Right wheel drive problem", + "wheel_motor_blocked_left": "Left wheel motor blocked", + "wheel_motor_blocked_rear_left": "Rear left wheel motor blocked", + "wheel_motor_blocked_rear_right": "Rear right wheel motor blocked", + "wheel_motor_blocked_right": "Right wheel motor blocked", + "wheel_motor_overloaded_left": "Left wheel motor overloaded", + "wheel_motor_overloaded_rear_left": "Rear left wheel motor overloaded", + "wheel_motor_overloaded_rear_right": "Rear right wheel motor overloaded", + "wheel_motor_overloaded_right": "Right wheel motor overloaded", + "work_area_not_valid": "Work area not valid", + "wrong_loop_signal": "Wrong loop signal", + "wrong_pin_code": "Wrong PIN code", + "zone_generator_problem": "Zone generator problem" + } + }, "number_of_charging_cycles": { "name": "Number of charging cycles" }, @@ -58,6 +191,21 @@ "cutting_blade_usage_time": { "name": "Cutting blade usage time" }, + "restricted_reason": { + "name": "Restricted reason", + "state": { + "none": "No restrictions", + "week_schedule": "Week schedule", + "park_override": "Park override", + "sensor": "Weather timer", + "daily_limit": "Daily limit", + "fota": "Firmware Over-the-Air update running", + "frost": "Frost", + "all_work_areas_completed": "All work areas completed", + "external": "External", + "not_applicable": "Not applicable" + } + }, "total_charging_time": { "name": "Total charging time" }, diff --git a/homeassistant/components/huum/config_flow.py b/homeassistant/components/huum/config_flow.py index e2ea2a7dbe1..5de94260a4b 100644 --- a/homeassistant/components/huum/config_flow.py +++ b/homeassistant/components/huum/config_flow.py @@ -9,7 +9,7 @@ from huum.exceptions import Forbidden, NotAuthenticated from huum.huum import Huum 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.helpers.aiohttp_client import async_get_clientsession @@ -25,14 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class HuumConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HuumConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for huum.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 541d4211e49..b4e14c42709 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -1,56 +1,29 @@ """Support for Hydrawise cloud.""" -from pydrawise import legacy -import voluptuous as vol +from pydrawise import auth, client -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_API_KEY, - CONF_SCAN_INTERVAL, - Platform, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.exceptions import ConfigEntryAuthFailed from .const import DOMAIN, SCAN_INTERVAL from .coordinator import HydrawiseDataUpdateCoordinator -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Hunter Hydrawise component.""" - if DOMAIN not in config: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_API_KEY: config[DOMAIN][CONF_ACCESS_TOKEN]}, - ) - ) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hydrawise from a config entry.""" - access_token = config_entry.data[CONF_API_KEY] - hydrawise = legacy.LegacyHydrawiseAsync(access_token) + if CONF_USERNAME not in config_entry.data or CONF_PASSWORD not in config_entry.data: + # The GraphQL API requires username and password to authenticate. If either is + # missing, reauth is required. + raise ConfigEntryAuthFailed + + hydrawise = client.Hydrawise( + auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]) + ) + coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index e75cf56ac75..a93976b12e0 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -3,20 +3,15 @@ from __future__ import annotations from pydrawise.schema import Zone -import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .coordinator import HydrawiseDataUpdateCoordinator @@ -39,27 +34,6 @@ BINARY_SENSOR_KEYS: list[str] = [ desc.key for desc in (BINARY_SENSOR_STATUS, *BINARY_SENSOR_TYPES) ] -# Deprecated since Home Assistant 2023.10.0 -# Can be removed completely in 2024.4.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSOR_KEYS)] - ) - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Hydrawise device.""" - # We don't need to trigger import flow from here as it's triggered from `__init__.py` - return # pragma: no cover - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index cfaaefcd03a..1c2c1c5cf29 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -2,18 +2,16 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from typing import Any from aiohttp import ClientError -from pydrawise import legacy +from pydrawise import auth, client +from pydrawise.exceptions import NotAuthorizedError import voluptuous as vol -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, FlowResultType -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN, LOGGER @@ -23,14 +21,26 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _create_entry( - self, api_key: str, *, on_failure: Callable[[str], ConfigFlowResult] + def __init__(self) -> None: + """Construct a ConfigFlow.""" + self.reauth_entry: ConfigEntry | None = None + + async def _create_or_update_entry( + self, + username: str, + password: str, + *, + on_failure: Callable[[str], ConfigFlowResult], ) -> ConfigFlowResult: """Create the config entry.""" - api = legacy.LegacyHydrawiseAsync(api_key) + + # Verify that the provided credentials work.""" + api = client.Hydrawise(auth.Auth(username, password)) try: # Skip fetching zones to save on metered API calls. - user = await api.get_user(fetch_zones=False) + user = await api.get_user() + except NotAuthorizedError: + return on_failure("invalid_auth") except TimeoutError: return on_failure("timeout_connect") except ClientError as ex: @@ -38,51 +48,33 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): return on_failure("cannot_connect") await self.async_set_unique_id(f"hydrawise-{user.customer_id}") - self._abort_if_unique_id_configured() - return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key}) + if not self.reauth_entry: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Hydrawise", + data={CONF_USERNAME: username, CONF_PASSWORD: password}, + ) - def _import_issue(self, error_type: str) -> ConfigFlowResult: - """Create an issue about a YAML import failure.""" - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_yaml_import_issue_{error_type}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.ERROR, - translation_key="deprecated_yaml_import_issue", - translation_placeholders={ - "error_type": error_type, - "url": "/config/integrations/dashboard/add?domain=hydrawise", - }, - ) - return self.async_abort(reason=error_type) - - def _deprecated_yaml_issue(self) -> None: - """Create an issue about YAML deprecation.""" - 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": "Hydrawise", - }, + self.hass.config_entries.async_update_entry( + self.reauth_entry, + data=self.reauth_entry.data + | {CONF_USERNAME: username, CONF_PASSWORD: password}, ) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> 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) + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + return await self._create_or_update_entry( + username=username, password=password, on_failure=self._show_form + ) return self._show_form() def _show_form(self, error_type: str | None = None) -> ConfigFlowResult: @@ -91,21 +83,17 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = error_type return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), errors=errors, ) - async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: - """Import data from YAML.""" - try: - result = await self._create_entry( - import_data.get(CONF_API_KEY, ""), - on_failure=self._import_issue, - ) - except AbortFlow: - self._deprecated_yaml_issue() - raise - - if result["type"] == FlowResultType.CREATE_ENTRY: - self._deprecated_yaml_issue() - return result + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth after updating config to username/password.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index eedeb4a07bc..84e9f979878 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -5,20 +5,16 @@ from __future__ import annotations from datetime import datetime from pydrawise.schema import Zone -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime +from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import DOMAIN @@ -39,32 +35,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -# Deprecated since Home Assistant 2023.10.0 -# Can be removed completely in 2024.4.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ) - } -) - TWO_YEAR_SECONDS = 60 * 60 * 24 * 365 * 2 WATERING_TIME_ICON = "mdi:water-pump" -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Hydrawise device.""" - # We don't need to trigger import flow from here as it's triggered from `__init__.py` - return # pragma: no cover - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index 8f079abcc7d..ee5cc0a541c 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -2,8 +2,11 @@ "config": { "step": { "user": { + "title": "Hydrawise Login", + "description": "Please provide the username and password for your Hydrawise cloud account:", "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -13,13 +16,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" - } - }, - "issues": { - "deprecated_yaml_import_issue": { - "title": "The Hydrawise YAML configuration import failed", - "description": "Configuring Hydrawise using YAML is being removed but there was an {error_type} error importing your YAML configuration.\n\nEnsure connection to Hydrawise works and restart Home Assistant to try again or remove the Hydrawise YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "entity": { diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 49106a5938a..2dc459e7dd4 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -6,28 +6,18 @@ from datetime import timedelta from typing import Any from pydrawise.schema import Zone -import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, SwitchDeviceClass, SwitchEntity, SwitchEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import ( - ALLOWED_WATERING_TIME, - CONF_WATERING_TIME, - DEFAULT_WATERING_TIME, - DOMAIN, -) +from .const import DEFAULT_WATERING_TIME, DOMAIN from .coordinator import HydrawiseDataUpdateCoordinator from .entity import HydrawiseEntity @@ -46,30 +36,6 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] -# Deprecated since Home Assistant 2023.10.0 -# Can be removed completely in 2024.4.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCH_KEYS): vol.All( - cv.ensure_list, [vol.In(SWITCH_KEYS)] - ), - vol.Optional( - CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME.total_seconds() // 60 - ): vol.All(vol.In(ALLOWED_WATERING_TIME)), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Hydrawise device.""" - # We don't need to trigger import flow from here as it's triggered from `__init__.py` - return # pragma: no cover - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index e29caa27ef7..64a9831800f 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -344,7 +344,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): # UI to approve the request for a new token). assert self._auth_id is not None self._request_token_task = self.hass.async_create_task( - self._request_token_task_func(self._auth_id) + self._request_token_task_func(self._auth_id), eager_start=False ) return self.async_external_step( step_id="create_token_external", url=self._get_hyperion_url() @@ -412,12 +412,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): entry = await self.async_set_unique_id(hyperion_id, raise_on_progress=False) if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and entry is not None: - self.hass.config_entries.async_update_entry(entry, data=self._data) - # Need to manually reload, as the listener won't have been installed because - # the initial load did not succeed (the reauth flow will not be initiated if - # the load succeeds) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + return self.async_update_reload_and_abort(entry, data=self._data) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 29576e9fc10..868b5a32c67 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -6,11 +6,13 @@ import logging from typing import Any from iaqualink.device import AqualinkThermostat +from iaqualink.systems.iaqua.device import AqualinkState from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry @@ -82,6 +84,16 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): else: _LOGGER.warning("Unknown operation mode: %s", hvac_mode) + @property + def hvac_action(self) -> HVACAction: + """Return the current HVAC action.""" + state = AqualinkState(self.dev._heater.state) + if state == AqualinkState.ON: + return HVACAction.HEATING + if state == AqualinkState.ENABLED: + return HVACAction.IDLE + return HVACAction.OFF + @property def target_temperature(self) -> float: """Return the current target temperature.""" diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 27181e80ed8..4f232220440 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -493,14 +493,12 @@ class IBeaconCoordinator: @callback def _async_restore_from_registry(self) -> None: """Restore the state of the Coordinator from the device registry.""" - for device in self._dev_reg.devices.values(): - unique_id = None - for identifier in device.identifiers: - if identifier[0] == DOMAIN: - unique_id = identifier[1] - break - if not unique_id: + for device in self._dev_reg.devices.get_devices_for_config_entry_id( + self._entry.entry_id + ): + if not (identifier := next(iter(device.identifiers), None)): continue + unique_id = identifier[1] # iBeacons with a fixed MAC address if unique_id.count("_") == 3: uuid, major, minor, address = unique_id.split("_") diff --git a/homeassistant/components/idasen_desk/__init__.py b/homeassistant/components/idasen_desk/__init__.py index 75ef70fcb50..2edd04b1d59 100644 --- a/homeassistant/components/idasen_desk/__init__.py +++ b/homeassistant/components/idasen_desk/__init__.py @@ -75,7 +75,7 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): # pylint: disab if not self.desk.is_connected: _LOGGER.debug("Desk disconnected. Reconnecting") self._connection_lost = True - self.hass.async_create_task(self.async_connect()) + self.hass.async_create_task(self.async_connect(), eager_start=False) elif self._connection_lost: _LOGGER.info("Reconnected to desk") self._connection_lost = False diff --git a/homeassistant/components/ihc/service_functions.py b/homeassistant/components/ihc/service_functions.py index cfd91f0960c..61eba4791ac 100644 --- a/homeassistant/components/ihc/service_functions.py +++ b/homeassistant/components/ihc/service_functions.py @@ -90,24 +90,24 @@ def setup_service_functions(hass: HomeAssistant) -> None: ihc_controller = _get_controller(call) await async_pulse(hass, ihc_controller, ihc_id) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_BOOL, async_set_runtime_value_bool, schema=SET_RUNTIME_VALUE_BOOL_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_INT, async_set_runtime_value_int, schema=SET_RUNTIME_VALUE_INT_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_FLOAT, async_set_runtime_value_float, schema=SET_RUNTIME_VALUE_FLOAT_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_PULSE, async_pulse_runtime_input, schema=PULSE_SCHEMA ) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 47767a004cb..f40958a28ea 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -7,9 +7,10 @@ import collections from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta +from functools import cached_property import logging from random import SystemRandom -from typing import TYPE_CHECKING, Final, final +from typing import Final, final from aiohttp import hdrs, web import httpx @@ -17,7 +18,7 @@ import httpx 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 +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -26,7 +27,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( - EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) @@ -35,12 +35,6 @@ from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from .const import DOMAIN, IMAGE_TIMEOUT -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=30) @@ -88,8 +82,7 @@ async def _async_get_image(image_entity: ImageEntity, timeout: int) -> Image: async with asyncio.timeout(timeout): if image_bytes := await image_entity.async_image(): content_type = valid_image_content_type(image_entity.content_type) - image = Image(content_type, image_bytes) - return image + return Image(content_type, image_bytes) raise HomeAssistantError("Unable to get image") @@ -199,7 +192,6 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): url, timeout=GET_IMAGE_TIMEOUT, follow_redirects=True ) response.raise_for_status() - return response except httpx.TimeoutException: _LOGGER.error("%s: Timeout getting image from %s", self.entity_id, url) return None @@ -211,6 +203,7 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): err, ) return None + return response async def _async_load_image_from_url(self, url: str) -> Image | None: """Load an image by url.""" diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 7504446f3fb..f39a78925c1 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -2,19 +2,34 @@ from __future__ import annotations -from aioimaplib import IMAP4_SSL, AioImapException +import asyncio +import logging +from typing import TYPE_CHECKING + +from aioimaplib import IMAP4_SSL, AioImapException, Response +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, ConfigEntryNotReady, + ServiceValidationError, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( + ImapMessage, ImapPollingDataUpdateCoordinator, ImapPushDataUpdateCoordinator, connect_to_server, @@ -23,6 +38,202 @@ from .errors import InvalidAuth, InvalidFolder PLATFORMS: list[Platform] = [Platform.SENSOR] +CONF_ENTRY = "entry" +CONF_SEEN = "seen" +CONF_UID = "uid" +CONF_TARGET_FOLDER = "target_folder" + +_LOGGER = logging.getLogger(__name__) + + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + +_SERVICE_UID_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTRY): cv.string, + vol.Required(CONF_UID): cv.string, + } +) + +SERVICE_SEEN_SCHEMA = _SERVICE_UID_SCHEMA +SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend( + { + vol.Optional(CONF_SEEN): cv.boolean, + vol.Required(CONF_TARGET_FOLDER): cv.string, + } +) +SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA +SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA + + +async def async_get_imap_client(hass: HomeAssistant, entry_id: str) -> IMAP4_SSL: + """Get IMAP client and connect.""" + if hass.data[DOMAIN].get(entry_id) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_entry", + ) + entry = hass.config_entries.async_get_entry(entry_id) + if TYPE_CHECKING: + assert entry is not None + try: + client = await connect_to_server(entry.data) + except InvalidAuth as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="invalid_auth" + ) from exc + except InvalidFolder as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="invalid_folder" + ) from exc + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + return client + + +@callback +def raise_on_error(response: Response, translation_key: str) -> None: + """Get error message from response.""" + if response.result != "OK": + error: str = response.lines[0].decode("utf-8") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=translation_key, + translation_placeholders={"error": error}, + ) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up imap services.""" + + async def async_seen(call: ServiceCall) -> None: + """Process mark as seen service call.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + _LOGGER.debug( + "Mark message %s as seen. Entry: %s", + uid, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + response = await client.store(uid, "+FLAGS (\\Seen)") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + raise_on_error(response, "seen_failed") + await client.close() + + hass.services.async_register(DOMAIN, "seen", async_seen, SERVICE_SEEN_SCHEMA) + + async def async_move(call: ServiceCall) -> None: + """Process move email service call.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + seen = bool(call.data.get(CONF_SEEN)) + target_folder: str = call.data[CONF_TARGET_FOLDER] + _LOGGER.debug( + "Move message %s to folder %s. Mark as seen: %s. Entry: %s", + uid, + target_folder, + seen, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + if seen: + response = await client.store(uid, "+FLAGS (\\Seen)") + raise_on_error(response, "seen_failed") + response = await client.copy(uid, target_folder) + raise_on_error(response, "copy_failed") + response = await client.store(uid, "+FLAGS (\\Deleted)") + raise_on_error(response, "delete_failed") + response = await asyncio.wait_for( + client.protocol.expunge(uid, by_uid=True), client.timeout + ) + raise_on_error(response, "expunge_failed") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + await client.close() + + hass.services.async_register(DOMAIN, "move", async_move, SERVICE_MOVE_SCHEMA) + + async def async_delete(call: ServiceCall) -> None: + """Process deleting email service call.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + _LOGGER.debug( + "Delete message %s. Entry: %s", + uid, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + response = await client.store(uid, "+FLAGS (\\Deleted)") + raise_on_error(response, "delete_failed") + response = await asyncio.wait_for( + client.protocol.expunge(uid, by_uid=True), client.timeout + ) + raise_on_error(response, "expunge_failed") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + await client.close() + + hass.services.async_register(DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA) + + async def async_fetch(call: ServiceCall) -> ServiceResponse: + """Process fetch email service and return content.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + _LOGGER.debug( + "Fetch text for message %s. Entry: %s", + uid, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + response = await client.fetch(uid, "BODY.PEEK[]") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + raise_on_error(response, "fetch_failed") + message = ImapMessage(response.lines[1]) + await client.close() + return { + "text": message.text, + "sender": message.sender, + "subject": message.subject, + "uid": uid, + } + + hass.services.async_register( + DOMAIN, + "fetch", + async_fetch, + SERVICE_FETCH_TEXT_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up imap from a config entry.""" diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 414d5830bae..62ed4d42a07 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -33,6 +33,7 @@ from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, CONF_ENABLE_PUSH, + CONF_EVENT_MESSAGE_DATA, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -42,6 +43,7 @@ from .const import ( DEFAULT_PORT, DOMAIN, MAX_MESSAGE_SIZE_LIMIT, + MESSAGE_DATA_OPTIONS, ) from .coordinator import connect_to_server from .errors import InvalidAuth, InvalidFolder @@ -55,6 +57,13 @@ CIPHER_SELECTOR = SelectSelector( ) ) TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig()) +EVENT_MESSAGE_DATA_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=MESSAGE_DATA_OPTIONS, + translation_key=CONF_EVENT_MESSAGE_DATA, + multiple=True, + ) +) CONFIG_SCHEMA = vol.Schema( { @@ -65,6 +74,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_CHARSET, default="utf-8"): str, vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + # The default for new entries is to not include text and headers + vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list, } ) CONFIG_SCHEMA_ADVANCED = { @@ -78,6 +89,10 @@ OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + # The default for older entries is to include text and headers + vol.Optional( + CONF_EVENT_MESSAGE_DATA, default=MESSAGE_DATA_OPTIONS + ): EVENT_MESSAGE_DATA_SELECTOR, } ) diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py index fd3da28971e..a341a2a55e7 100644 --- a/homeassistant/components/imap/const.py +++ b/homeassistant/components/imap/const.py @@ -8,7 +8,8 @@ CONF_SERVER: Final = "server" CONF_FOLDER: Final = "folder" CONF_SEARCH: Final = "search" CONF_CHARSET: Final = "charset" -CONF_MAX_MESSAGE_SIZE = "max_message_size" +CONF_EVENT_MESSAGE_DATA: Final = "event_message_data" +CONF_MAX_MESSAGE_SIZE: Final = "max_message_size" CONF_CUSTOM_EVENT_DATA_TEMPLATE: Final = "custom_event_data_template" CONF_SSL_CIPHER_LIST: Final = "ssl_cipher_list" CONF_ENABLE_PUSH: Final = "enable_push" @@ -17,4 +18,6 @@ DEFAULT_PORT: Final = 993 DEFAULT_MAX_MESSAGE_SIZE = 2048 -MAX_MESSAGE_SIZE_LIMIT = 30000 +MESSAGE_DATA_OPTIONS: Final = ["text", "headers"] + +MAX_MESSAGE_SIZE_LIMIT: Final = 30000 diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 78b52e06db3..c0123b89ee4 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -1,4 +1,4 @@ -"""Coordinator for imag integration.""" +"""Coordinator for imap integration.""" from __future__ import annotations @@ -41,6 +41,7 @@ from homeassistant.util.ssl import ( from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, + CONF_EVENT_MESSAGE_DATA, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -48,6 +49,7 @@ from .const import ( CONF_SSL_CIPHER_LIST, DEFAULT_MAX_MESSAGE_SIZE, DOMAIN, + MESSAGE_DATA_OPTIONS, ) from .errors import InvalidAuth, InvalidFolder @@ -123,13 +125,13 @@ class ImapMessage: return str(part.get_payload()) @property - def headers(self) -> dict[str, tuple[str,]]: + def headers(self) -> dict[str, tuple[str, ...]]: """Get the email headers.""" - header_base: dict[str, tuple[str,]] = {} + header_base: dict[str, tuple[str, ...]] = {} for key, value in self.email_message.items(): - header_instances: tuple[str,] = (str(value),) + header_instances: tuple[str, ...] = (str(value),) if header_base.setdefault(key, header_instances) != header_instances: - header_base[key] += header_instances # type: ignore[assignment] + header_base[key] += header_instances return header_base @property @@ -225,6 +227,12 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): self._last_message_id: str | None = None self.custom_event_template = None self._diagnostics_data: dict[str, Any] = {} + self._event_data_keys: list[str] = entry.data.get( + CONF_EVENT_MESSAGE_DATA, MESSAGE_DATA_OPTIONS + ) + self._max_event_size: int = entry.data.get( + CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE + ) _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) if _custom_event_template is not None: self.custom_event_template = Template(_custom_event_template, hass=hass) @@ -254,17 +262,18 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): initial = False self._last_message_id = message_id data = { + "entry_id": self.config_entry.entry_id, "server": self.config_entry.data[CONF_SERVER], "username": self.config_entry.data[CONF_USERNAME], "search": self.config_entry.data[CONF_SEARCH], "folder": self.config_entry.data[CONF_FOLDER], "initial": initial, "date": message.date, - "text": message.text, "sender": message.sender, "subject": message.subject, - "headers": message.headers, + "uid": last_message_uid, } + data.update({key: getattr(message, key) for key in self._event_data_keys}) if self.custom_event_template is not None: try: data["custom"] = self.custom_event_template.async_render( @@ -287,11 +296,8 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): last_message_uid, err, ) - data["text"] = message.text[ - : self.config_entry.data.get( - CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE - ) - ] + if "text" in data: + data["text"] = message.text[: self._max_event_size] self._update_diagnostics(data) if (size := len(json_bytes(data))) > MAX_EVENT_DATA_BYTES: _LOGGER.warning( @@ -397,8 +403,6 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): """Update the number of unread emails.""" try: messages = await self._async_fetch_number_of_messages() - self.auth_errors = 0 - return messages except ( AioImapException, UpdateFailed, @@ -425,6 +429,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): self.async_set_update_error(ex) raise ConfigEntryAuthFailed from ex + self.auth_errors = 0 + return messages + class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): """Class for imap client.""" @@ -436,11 +443,12 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): _LOGGER.debug("Connected to server %s using IMAP push", entry.data[CONF_SERVER]) super().__init__(hass, imap_client, entry, None) self._push_wait_task: asyncio.Task[None] | None = None + self.number_of_messages: int | None = None async def _async_update_data(self) -> int | None: """Update the number of unread emails.""" await self.async_start() - return None + return self.number_of_messages async def async_start(self) -> None: """Start coordinator.""" @@ -452,7 +460,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): """Wait for data push from server.""" while True: try: - number_of_messages = await self._async_fetch_number_of_messages() + self.number_of_messages = await self._async_fetch_number_of_messages() except InvalidAuth as ex: self.auth_errors += 1 await self._cleanup() @@ -482,7 +490,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): continue else: self.auth_errors = 0 - self.async_set_updated_data(number_of_messages) + self.async_set_updated_data(self.number_of_messages) try: idle: asyncio.Future = await self.imap_client.idle_start() await self.imap_client.wait_server_push() diff --git a/homeassistant/components/imap/diagnostics.py b/homeassistant/components/imap/diagnostics.py index 467f19d6338..8afe3e327ba 100644 --- a/homeassistant/components/imap/diagnostics.py +++ b/homeassistant/components/imap/diagnostics.py @@ -31,9 +31,7 @@ def _async_get_diagnostics( redacted_config = async_redact_data(entry.data, REDACT_CONFIG) coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - data = { + return { "config": redacted_config, "event": coordinator.diagnostics_data, } - - return data diff --git a/homeassistant/components/imap/icons.json b/homeassistant/components/imap/icons.json index a4a79aef60e..6672f9a4a7f 100644 --- a/homeassistant/components/imap/icons.json +++ b/homeassistant/components/imap/icons.json @@ -8,5 +8,11 @@ } } } + }, + "services": { + "seen": "mdi:email-open-outline", + "move": "mdi:email-arrow-right-outline", + "delete": "mdi:trash-can-outline", + "fetch": "mdi:email-sync-outline" } } diff --git a/homeassistant/components/imap/services.yaml b/homeassistant/components/imap/services.yaml new file mode 100644 index 00000000000..be56eb148da --- /dev/null +++ b/homeassistant/components/imap/services.yaml @@ -0,0 +1,58 @@ +seen: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + required: true + example: "12" + selector: + text: +move: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + required: true + example: "12" + selector: + text: + seen: + selector: + boolean: + target_folder: + required: true + example: "INBOX.Trash" + selector: + text: + +delete: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + example: "12" + required: true + selector: + text: + +fetch: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + required: true + example: "12" + selector: + text: diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index c332e3e8edb..115d46f3d0e 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -35,6 +35,35 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, + "exceptions": { + "copy_failed": { + "message": "Copying the message failed with \"{error}\"." + }, + "delete_failed": { + "message": "Marking the message for deletion failed with \"{error}\"." + }, + "expunge_failed": { + "message": "Expunging the message failed with \"{error}\"." + }, + "fetch_failed": { + "message": "Fetching the message text failed with \"{error}\"." + }, + "invalid_entry": { + "message": "No valid IMAP entry was found." + }, + "invalid_auth": { + "message": "[%key:common::config_flow::error::invalid_auth%]" + }, + "invalid_folder": { + "message": "[%key:component::imap::config::error::invalid_folder%]" + }, + "imap_server_fail": { + "message": "The IMAP server failed to connect: {error}." + }, + "seen_failed": { + "message": "Marking message as seen failed with \"{error}\"." + } + }, "options": { "step": { "init": { @@ -43,7 +72,8 @@ "search": "[%key:component::imap::config::step::user::data::search%]", "custom_event_data_template": "Template to create custom event data", "max_message_size": "Max message size (2048 < size < 30000)", - "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable." + "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable.", + "event_message_data": "Message data to be included in the `imap_content` event data:" } } }, @@ -63,6 +93,78 @@ "modern": "Modern ciphers", "intermediate": "Intermediate ciphers" } + }, + "event_message_data": { + "options": { + "text": "Body text", + "headers": "Message headers" + } + } + }, + "services": { + "fetch": { + "name": "Fetch message", + "description": "Fetch the email message from the server.", + "fields": { + "entry": { + "name": "Entry", + "description": "The IMAP config entry." + }, + "uid": { + "name": "UID", + "description": "The email identifier (UID)." + } + } + }, + "seen": { + "name": "Mark message as seen", + "description": "Mark an email as seen.", + "fields": { + "entry": { + "name": "Entry", + "description": "The IMAP config entry." + }, + "uid": { + "name": "UID", + "description": "The email identifier (UID)." + } + } + }, + "move": { + "name": "Move message", + "description": "Move an email to a target folder.", + "fields": { + "entry": { + "name": "[%key:component::imap::services::seen::fields::entry::name%]", + "description": "[%key:component::imap::services::seen::fields::entry::description%]" + }, + "seen": { + "name": "Seen", + "description": "Mark the email as seen." + }, + "uid": { + "name": "[%key:component::imap::services::seen::fields::uid::name%]", + "description": "[%key:component::imap::services::seen::fields::uid::description%]" + }, + "target_folder": { + "name": "Target folder", + "description": "The target folder the email should be moved to." + } + } + }, + "delete": { + "name": "Delete message", + "description": "Delete an email.", + "fields": { + "entry": { + "name": "[%key:component::imap::services::seen::fields::entry::name%]", + "description": "[%key:component::imap::services::seen::fields::entry::description%]" + }, + "uid": { + "name": "[%key:component::imap::services::seen::fields::uid::name%]", + "description": "[%key:component::imap::services::seen::fields::uid::description%]" + } + } } } } diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index a1a2d6b1b65..370b244dac2 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -326,7 +326,9 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN): return if not self._provision_task: - self._provision_task = self.hass.async_create_task(_do_provision()) + self._provision_task = self.hass.async_create_task( + _do_provision(), eager_start=False + ) if not self._provision_task.done(): return self.async_show_progress( @@ -372,7 +374,9 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN): except AbortFlow as err: return self.async_abort(reason=err.reason) - self._authorize_task = self.hass.async_create_task(authorized_event.wait()) + self._authorize_task = self.hass.async_create_task( + authorized_event.wait(), eager_start=False + ) if not self._authorize_task.done(): return self.async_show_progress( diff --git a/homeassistant/components/improv_ble/strings.json b/homeassistant/components/improv_ble/strings.json index b5713910134..be157b8070d 100644 --- a/homeassistant/components/improv_ble/strings.json +++ b/homeassistant/components/improv_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/inkbird/strings.json b/homeassistant/components/inkbird/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/inkbird/strings.json +++ b/homeassistant/components/inkbird/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 52788066ba2..55b43ee8a1e 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -264,7 +264,7 @@ class InputText(collection.CollectionEntity, RestoreEntity): return state = await self.async_get_last_state() - value: str | None = state and state.state # type: ignore[assignment] + value = state.state if state else None # Check against None because value can be 0 if value is not None and self._minimum <= len(value) <= self._maximum: diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 529ac20df52..0ec2434bc82 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers.typing import ConfigType from . import api from .const import ( CONF_CAT, + CONF_DEV_PATH, CONF_DIM_STEPS, CONF_HOUSECODE, CONF_OVERRIDE, @@ -84,6 +85,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Insteon entry.""" + if dev_path := entry.options.get(CONF_DEV_PATH): + hass.data[DOMAIN] = {} + hass.data[DOMAIN][CONF_DEV_PATH] = dev_path + + api.async_load_api(hass) + await api.async_register_insteon_frontend(hass) + if not devices.modem: try: await async_connect(**entry.data) @@ -149,9 +157,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_insteon_device(hass, devices.modem, entry.entry_id) - api.async_load_api(hass) - await api.async_register_insteon_frontend(hass) - entry.async_create_background_task( hass, async_get_device_config(hass, entry), "insteon-get-device-config" ) diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index fa006c6a6d9..1f671aa1343 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -16,10 +16,19 @@ from .aldb import ( websocket_reset_aldb, websocket_write_aldb, ) +from .config import ( + websocket_add_device_override, + websocket_get_config, + websocket_get_modem_schema, + websocket_remove_device_override, + websocket_update_modem_config, +) from .device import ( websocket_add_device, + websocket_add_x10_device, websocket_cancel_add_device, websocket_get_device, + websocket_remove_device, ) from .properties import ( websocket_change_properties_record, @@ -58,6 +67,8 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_reset_aldb) websocket_api.async_register_command(hass, websocket_add_default_links) websocket_api.async_register_command(hass, websocket_notify_on_aldb_status) + websocket_api.async_register_command(hass, websocket_add_x10_device) + websocket_api.async_register_command(hass, websocket_remove_device) websocket_api.async_register_command(hass, websocket_get_properties) websocket_api.async_register_command(hass, websocket_change_properties_record) @@ -65,6 +76,12 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_load_properties) websocket_api.async_register_command(hass, websocket_reset_properties) + websocket_api.async_register_command(hass, websocket_get_config) + websocket_api.async_register_command(hass, websocket_get_modem_schema) + websocket_api.async_register_command(hass, websocket_update_modem_config) + websocket_api.async_register_command(hass, websocket_add_device_override) + websocket_api.async_register_command(hass, websocket_remove_device_override) + async def async_register_insteon_frontend(hass: HomeAssistant): """Register the Insteon frontend configuration panel.""" @@ -80,8 +97,7 @@ async def async_register_insteon_frontend(hass: HomeAssistant): hass=hass, frontend_url_path=DOMAIN, webcomponent_name="insteon-frontend", - sidebar_title=DOMAIN.capitalize(), - sidebar_icon="mdi:power", + config_panel_domain=DOMAIN, module_url=f"{URL_BASE}/entrypoint-{build_id}.js", embed_iframe=True, require_admin=True, diff --git a/homeassistant/components/insteon/api/config.py b/homeassistant/components/insteon/api/config.py new file mode 100644 index 00000000000..8a617911d1e --- /dev/null +++ b/homeassistant/components/insteon/api/config.py @@ -0,0 +1,272 @@ +"""API calls to manage Insteon configuration changes.""" + +from __future__ import annotations + +from typing import Any, TypedDict + +from pyinsteon import async_close, async_connect, devices +from pyinsteon.address import Address +import voluptuous as vol +import voluptuous_serialize + +from homeassistant.components import websocket_api +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_DEVICE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from ..const import ( + CONF_HOUSECODE, + CONF_OVERRIDE, + CONF_UNITCODE, + CONF_X10, + DEVICE_ADDRESS, + DOMAIN, + ID, + SIGNAL_ADD_DEVICE_OVERRIDE, + SIGNAL_ADD_X10_DEVICE, + SIGNAL_REMOVE_DEVICE_OVERRIDE, + TYPE, +) +from ..schemas import ( + build_device_override_schema, + build_hub_schema, + build_plm_manual_schema, + build_plm_schema, +) +from ..utils import async_get_usb_ports + +HUB_V1_SCHEMA = build_hub_schema(hub_version=1) +HUB_V2_SCHEMA = build_hub_schema(hub_version=2) +PLM_SCHEMA = build_plm_manual_schema() +DEVICE_OVERRIDE_SCHEMA = build_device_override_schema() +OVERRIDE = "override" + + +class X10DeviceConfig(TypedDict): + """X10 Device Configuration Definition.""" + + housecode: str + unitcode: int + platform: str + dim_steps: int + + +class DeviceOverride(TypedDict): + """X10 Device Configuration Definition.""" + + address: Address | str + cat: int + subcat: str + + +def get_insteon_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Return the Insteon configuration entry.""" + return hass.config_entries.async_entries(DOMAIN)[0] + + +def add_x10_device(hass: HomeAssistant, x10_device: X10DeviceConfig): + """Add an X10 device to the Insteon integration.""" + + config_entry = get_insteon_config_entry(hass) + x10_config = config_entry.options.get(CONF_X10, []) + if any( + device[CONF_HOUSECODE] == x10_device["housecode"] + and device[CONF_UNITCODE] == x10_device["unitcode"] + for device in x10_config + ): + raise ValueError("Duplicate X10 device") + + hass.config_entries.async_update_entry( + entry=config_entry, + options=config_entry.options | {CONF_X10: [*x10_config, x10_device]}, + ) + async_dispatcher_send(hass, SIGNAL_ADD_X10_DEVICE, x10_device) + + +def remove_x10_device(hass: HomeAssistant, housecode: str, unitcode: int): + """Remove an X10 device from the config.""" + + config_entry = get_insteon_config_entry(hass) + new_options = {**config_entry.options} + new_x10 = [ + existing_device + for existing_device in config_entry.options.get(CONF_X10, []) + if existing_device[CONF_HOUSECODE].lower() != housecode.lower() + or existing_device[CONF_UNITCODE] != unitcode + ] + + new_options[CONF_X10] = new_x10 + hass.config_entries.async_update_entry(entry=config_entry, options=new_options) + + +def add_device_overide(hass: HomeAssistant, override: DeviceOverride): + """Add an Insteon device override.""" + + config_entry = get_insteon_config_entry(hass) + override_config = config_entry.options.get(CONF_OVERRIDE, []) + address = Address(override[CONF_ADDRESS]) + if any( + Address(existing_override[CONF_ADDRESS]) == address + for existing_override in override_config + ): + raise ValueError("Duplicate override") + + hass.config_entries.async_update_entry( + entry=config_entry, + options=config_entry.options | {CONF_OVERRIDE: [*override_config, override]}, + ) + async_dispatcher_send(hass, SIGNAL_ADD_DEVICE_OVERRIDE, override) + + +def remove_device_override(hass: HomeAssistant, address: Address): + """Remove a device override from config.""" + + config_entry = get_insteon_config_entry(hass) + new_options = {**config_entry.options} + + new_overrides = [ + existing_override + for existing_override in config_entry.options.get(CONF_OVERRIDE, []) + if Address(existing_override[CONF_ADDRESS]) != address + ] + new_options[CONF_OVERRIDE] = new_overrides + hass.config_entries.async_update_entry(entry=config_entry, options=new_options) + + +async def _async_connect(**kwargs): + """Connect to the Insteon modem.""" + if devices.modem: + await async_close() + try: + await async_connect(**kwargs) + except ConnectionError: + return False + return True + + +@websocket_api.websocket_command({vol.Required(TYPE): "insteon/config/get"}) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_config( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get Insteon configuration.""" + config_entry = get_insteon_config_entry(hass) + modem_config = config_entry.data + options_config = config_entry.options + x10_config = options_config.get(CONF_X10) + override_config = options_config.get(CONF_OVERRIDE) + connection.send_result( + msg[ID], + { + "modem_config": {**modem_config}, + "x10_config": x10_config, + "override_config": override_config, + }, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/get_modem_schema", + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_modem_schema( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + config_entry = get_insteon_config_entry(hass) + config_data = config_entry.data + if device := config_data.get(CONF_DEVICE): + ports = await async_get_usb_ports(hass=hass) + plm_schema = voluptuous_serialize.convert( + build_plm_schema(ports=ports, device=device) + ) + connection.send_result(msg[ID], plm_schema) + else: + hub_schema = voluptuous_serialize.convert(build_hub_schema(**config_data)) + connection.send_result(msg[ID], hub_schema) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/update_modem_config", + vol.Required("config"): vol.Any(PLM_SCHEMA, HUB_V2_SCHEMA, HUB_V1_SCHEMA), + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_update_modem_config( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + config = msg["config"] + config_entry = get_insteon_config_entry(hass) + is_connected = devices.modem.connected + + if not await _async_connect(**config): + connection.send_error( + msg_id=msg[ID], code="connection_failed", message="Connection failed" + ) + # Try to reconnect using old info + if is_connected: + await _async_connect(**config_entry.data) + return + + hass.config_entries.async_update_entry( + entry=config_entry, + data=config, + ) + connection.send_result(msg[ID], {"status": "success"}) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/device_override/add", + vol.Required(OVERRIDE): DEVICE_OVERRIDE_SCHEMA, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_add_device_override( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + override = msg[OVERRIDE] + try: + add_device_overide(hass, override) + except ValueError: + connection.send_error(msg[ID], "duplicate", "Duplicate device address") + + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/device_override/remove", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_remove_device_override( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + address = Address(msg[DEVICE_ADDRESS]) + remove_device_override(hass, address) + async_dispatcher_send(hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, address) + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index d48d87fa347..e8bd08bc4ee 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -3,12 +3,14 @@ from typing import Any from pyinsteon import devices +from pyinsteon.address import Address from pyinsteon.constants import DeviceAction import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( DEVICE_ADDRESS, @@ -18,8 +20,17 @@ from ..const import ( ID, INSTEON_DEVICE_NOT_FOUND, MULTIPLE, + SIGNAL_REMOVE_HA_DEVICE, + SIGNAL_REMOVE_INSTEON_DEVICE, + SIGNAL_REMOVE_X10_DEVICE, TYPE, ) +from ..schemas import build_x10_schema +from .config import add_x10_device, remove_device_override, remove_x10_device + +X10_DEVICE = "x10_device" +X10_DEVICE_SCHEMA = build_x10_schema() +REMOVE_ALL_REFS = "remove_all_refs" def compute_device_name(ha_device): @@ -139,3 +150,61 @@ async def websocket_cancel_add_device( """Cancel the Insteon all-linking process.""" await devices.async_cancel_all_linking() connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/device/remove", + vol.Required(DEVICE_ADDRESS): str, + vol.Required(REMOVE_ALL_REFS): bool, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_remove_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Remove an Insteon device.""" + + address = msg[DEVICE_ADDRESS] + remove_all_refs = msg[REMOVE_ALL_REFS] + if address.startswith("X10"): + _, housecode, unitcode = address.split(".") + unitcode = int(unitcode) + async_dispatcher_send(hass, SIGNAL_REMOVE_X10_DEVICE, housecode, unitcode) + remove_x10_device(hass, housecode, unitcode) + else: + address = Address(address) + remove_device_override(hass, address) + async_dispatcher_send(hass, SIGNAL_REMOVE_HA_DEVICE, address) + async_dispatcher_send( + hass, SIGNAL_REMOVE_INSTEON_DEVICE, address, remove_all_refs + ) + + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/device/add_x10", + vol.Required(X10_DEVICE): X10_DEVICE_SCHEMA, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_add_x10_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the X10 devices configuration.""" + x10_device = msg[X10_DEVICE] + try: + add_x10_device(hass, x10_device) + except ValueError: + connection.send_error(msg[ID], code="duplicate", message="Duplicate X10 device") + return + + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 7fac5439f56..20e798dded0 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -139,8 +139,7 @@ def property_to_dict(prop): modified = value == prop.new_value if prop.value_type in [ToggleMode, RelayMode] or prop.name == RAMP_RATE_IN_SEC: value = str(value).lower() - prop_dict = {"name": prop.name, "value": value, "modified": modified} - return prop_dict + return {"name": prop.name, "value": value, "modified": modified} def update_property(device, prop_name, value): diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index 7eac51c600e..baf06b13860 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -4,52 +4,19 @@ from __future__ import annotations import logging -from pyinsteon import async_close, async_connect, devices +from pyinsteon import async_connect 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, - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) -from homeassistant.core import callback +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_NAME from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ( - CONF_HOUSECODE, - CONF_HUB_VERSION, - CONF_OVERRIDE, - CONF_UNITCODE, - CONF_X10, - DOMAIN, - SIGNAL_ADD_DEVICE_OVERRIDE, - SIGNAL_ADD_X10_DEVICE, - SIGNAL_REMOVE_DEVICE_OVERRIDE, - SIGNAL_REMOVE_X10_DEVICE, -) -from .schemas import ( - add_device_override, - add_x10_device, - build_device_override_schema, - build_hub_schema, - build_plm_manual_schema, - build_plm_schema, - build_remove_override_schema, - build_remove_x10_schema, - build_x10_schema, -) +from .const import CONF_HUB_VERSION, DOMAIN +from .schemas import build_hub_schema, build_plm_manual_schema, build_plm_schema from .utils import async_get_usb_ports STEP_PLM = "plm" @@ -72,46 +39,12 @@ async def _async_connect(**kwargs): """Connect to the Insteon modem.""" try: await async_connect(**kwargs) - _LOGGER.info("Connected to Insteon modem") - return True except ConnectionError: _LOGGER.error("Could not connect to Insteon modem") return False - -def _remove_override(address, options): - """Remove a device override from config.""" - new_options = {} - if options.get(CONF_X10): - new_options[CONF_X10] = options.get(CONF_X10) - 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 - - -def _remove_x10(device, options): - """Remove an X10 device from the config.""" - housecode = device[11].lower() - unitcode = int(device[24:]) - new_options = {} - if options.get(CONF_OVERRIDE): - new_options[CONF_OVERRIDE] = options.get(CONF_OVERRIDE) - new_x10 = [ - existing_device - for existing_device in options[CONF_X10] - if ( - existing_device[CONF_HOUSECODE].lower() != housecode - or existing_device[CONF_UNITCODE] != unitcode - ) - ] - if new_x10: - new_options[CONF_X10] = new_x10 - return new_options, housecode, unitcode + _LOGGER.info("Connected to Insteon modem") + return True class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): @@ -121,14 +54,6 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): _device_name: str | None = None discovered_conf: dict[str, str] = {} - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> InsteonOptionsFlowHandler: - """Define the config flow to handle options.""" - return InsteonOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): """Init the config flow.""" if self._async_current_entries(): @@ -236,140 +161,3 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): } await self.async_set_unique_id(format_mac(discovery_info.macaddress)) return await self.async_step_user() - - -class InsteonOptionsFlowHandler(OptionsFlow): - """Handle an Insteon options flow.""" - - 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) -> ConfigFlowResult: - """Init the options config flow.""" - menu_options = [STEP_ADD_OVERRIDE, STEP_ADD_X10] - - if self.config_entry.data.get(CONF_HOST): - menu_options.append(STEP_CHANGE_HUB_CONFIG) - else: - menu_options.append(STEP_CHANGE_PLM_CONFIG) - - options = {**self.config_entry.options} - if options.get(CONF_OVERRIDE): - menu_options.append(STEP_REMOVE_OVERRIDE) - if options.get(CONF_X10): - menu_options.append(STEP_REMOVE_X10) - - return self.async_show_menu(step_id="init", menu_options=menu_options) - - async def async_step_change_hub_config(self, user_input=None) -> ConfigFlowResult: - """Change the Hub configuration.""" - errors = {} - if user_input is not None: - data = { - **self.config_entry.data, - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - } - if self.config_entry.data[CONF_HUB_VERSION] == 2: - data[CONF_USERNAME] = user_input[CONF_USERNAME] - data[CONF_PASSWORD] = user_input[CONF_PASSWORD] - if devices.modem: - await async_close() - - if await _async_connect(**data): - self.hass.config_entries.async_update_entry( - self.config_entry, data=data - ) - return self.async_create_entry(data={**self.config_entry.options}) - errors["base"] = "cannot_connect" - data_schema = build_hub_schema(**self.config_entry.data) - return self.async_show_form( - step_id=STEP_CHANGE_HUB_CONFIG, data_schema=data_schema, errors=errors - ) - - async def async_step_change_plm_config(self, user_input=None) -> ConfigFlowResult: - """Change the PLM configuration.""" - errors = {} - if user_input is not None: - data = { - **self.config_entry.data, - CONF_DEVICE: user_input[CONF_DEVICE], - } - if devices.modem: - await async_close() - if await _async_connect(**data): - self.hass.config_entries.async_update_entry( - self.config_entry, data=data - ) - return self.async_create_entry(data={**self.config_entry.options}) - errors["base"] = "cannot_connect" - - ports = await async_get_usb_ports(self.hass) - data_schema = build_plm_schema(ports, **self.config_entry.data) - return self.async_show_form( - step_id=STEP_CHANGE_PLM_CONFIG, data_schema=data_schema, errors=errors - ) - - async def async_step_add_override(self, user_input=None) -> ConfigFlowResult: - """Add a device override.""" - errors = {} - if user_input is not None: - try: - data = add_device_override({**self.config_entry.options}, user_input) - async_dispatcher_send(self.hass, SIGNAL_ADD_DEVICE_OVERRIDE, user_input) - return self.async_create_entry(data=data) - except ValueError: - errors["base"] = "input_error" - schema_defaults = user_input if user_input is not None else {} - data_schema = build_device_override_schema(**schema_defaults) - return self.async_show_form( - step_id=STEP_ADD_OVERRIDE, data_schema=data_schema, errors=errors - ) - - 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: - options = add_x10_device({**self.config_entry.options}, user_input) - async_dispatcher_send(self.hass, SIGNAL_ADD_X10_DEVICE, user_input) - return self.async_create_entry(data=options) - schema_defaults: dict[str, str] = user_input if user_input is not None else {} - data_schema = build_x10_schema(**schema_defaults) - return self.async_show_form( - step_id=STEP_ADD_X10, data_schema=data_schema, errors=errors - ) - - async def async_step_remove_override(self, user_input=None) -> ConfigFlowResult: - """Remove a device override.""" - errors: dict[str, str] = {} - options = self.config_entry.options - if user_input is not None: - options = _remove_override(user_input[CONF_ADDRESS], options) - async_dispatcher_send( - self.hass, - SIGNAL_REMOVE_DEVICE_OVERRIDE, - user_input[CONF_ADDRESS], - ) - return self.async_create_entry(data=options) - - data_schema = build_remove_override_schema(options[CONF_OVERRIDE]) - return self.async_show_form( - step_id=STEP_REMOVE_OVERRIDE, data_schema=data_schema, errors=errors - ) - - async def async_step_remove_x10(self, user_input=None) -> ConfigFlowResult: - """Remove an X10 device.""" - errors: dict[str, str] = {} - options = self.config_entry.options - if user_input is not None: - options, housecode, unitcode = _remove_x10(user_input[CONF_DEVICE], options) - async_dispatcher_send( - self.hass, SIGNAL_REMOVE_X10_DEVICE, housecode, unitcode - ) - return self.async_create_entry(data=options) - - data_schema = build_remove_x10_schema(options[CONF_X10]) - return self.async_show_form( - step_id=STEP_REMOVE_X10, data_schema=data_schema, errors=errors - ) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index b7e6e6055e1..11e1943aa73 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -101,6 +101,8 @@ SIGNAL_SAVE_DEVICES = "save_devices" SIGNAL_ADD_ENTITIES = "insteon_add_entities" SIGNAL_ADD_DEFAULT_LINKS = "add_default_links" SIGNAL_ADD_DEVICE_OVERRIDE = "add_device_override" +SIGNAL_REMOVE_HA_DEVICE = "insteon_remove_ha_device" +SIGNAL_REMOVE_INSTEON_DEVICE = "insteon_remove_insteon_device" SIGNAL_REMOVE_DEVICE_OVERRIDE = "insteon_remove_device_override" SIGNAL_REMOVE_ENTITY = "insteon_remove_entity" SIGNAL_ADD_X10_DEVICE = "insteon_add_x10_device" diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index f81298dfe48..79e5c18a934 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -95,6 +95,7 @@ class InsteonEntity(Entity): f" {self._insteon_device.engine_version}" ), via_device=(DOMAIN, str(devices.modem.address)), + configuration_url=f"homeassistant://insteon/device/config/{self._insteon_device.id}", ) @callback diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index cf210963841..7d12436d0fb 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -18,7 +18,7 @@ "loggers": ["pyinsteon", "pypubsub"], "requirements": [ "pyinsteon==1.5.3", - "insteon-frontend-home-assistant==0.4.0" + "insteon-frontend-home-assistant==0.5.0" ], "usb": [ { diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index e277281c240..837c6224014 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -2,9 +2,6 @@ from __future__ import annotations -from binascii import Error as HexError, unhexlify - -from pyinsteon.address import Address from pyinsteon.constants import HC_LOOKUP import voluptuous as vol @@ -25,10 +22,8 @@ from .const import ( CONF_CAT, CONF_DIM_STEPS, CONF_HOUSECODE, - CONF_OVERRIDE, CONF_SUBCAT, CONF_UNITCODE, - CONF_X10, HOUSECODES, PORT_HUB_V1, PORT_HUB_V2, @@ -76,76 +71,6 @@ TRIGGER_SCENE_SCHEMA = vol.Schema( ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) -def normalize_byte_entry_to_int(entry: int | bytes | str): - """Format a hex entry value.""" - if isinstance(entry, int): - if entry in range(256): - return entry - raise ValueError("Must be single byte") - if isinstance(entry, str): - if entry[0:2].lower() == "0x": - entry = entry[2:] - if len(entry) != 2: - raise ValueError("Not a valid hex code") - try: - entry = unhexlify(entry) - except HexError as err: - raise ValueError("Not a valid hex code") from err - return int.from_bytes(entry, byteorder="big") - - -def add_device_override(config_data, new_override): - """Add a new device override.""" - try: - address = str(Address(new_override[CONF_ADDRESS])) - cat = normalize_byte_entry_to_int(new_override[CONF_CAT]) - subcat = normalize_byte_entry_to_int(new_override[CONF_SUBCAT]) - except ValueError as err: - raise ValueError("Incorrect values") from err - - 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): - new_config[CONF_X10] = config_data[CONF_X10] - new_config[CONF_OVERRIDE] = overrides - return new_config - - -def add_x10_device(config_data, new_x10): - """Add a new X10 device to X10 device list.""" - 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] - new_config[CONF_X10] = x10_devices - return new_config - - def build_device_override_schema( address=vol.UNDEFINED, cat=vol.UNDEFINED, @@ -169,12 +94,16 @@ def build_x10_schema( dim_steps=22, ): """Build the X10 schema for config flow.""" + if platform == "light": + dim_steps_schema = vol.Required(CONF_DIM_STEPS, default=dim_steps) + else: + dim_steps_schema = vol.Optional(CONF_DIM_STEPS, default=dim_steps) return vol.Schema( { vol.Required(CONF_HOUSECODE, default=housecode): vol.In(HC_LOOKUP.keys()), vol.Required(CONF_UNITCODE, default=unitcode): vol.In(range(1, 17)), vol.Required(CONF_PLATFORM, default=platform): vol.In(X10_PLATFORMS), - vol.Optional(CONF_DIM_STEPS, default=dim_steps): vol.In(range(1, 255)), + dim_steps_schema: vol.Range(min=0, max=255), } ) @@ -219,18 +148,3 @@ def build_hub_schema( schema[vol.Required(CONF_USERNAME, default=username)] = str schema[vol.Required(CONF_PASSWORD, default=password)] = str return vol.Schema(schema) - - -def build_remove_override_schema(data): - """Build the schema to remove device overrides in config flow options.""" - 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 = [ - 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/utils.py b/homeassistant/components/insteon/utils.py index 272018ea507..db25d8c97a9 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -65,6 +65,8 @@ from .const import ( SIGNAL_PRINT_ALDB, SIGNAL_REMOVE_DEVICE_OVERRIDE, SIGNAL_REMOVE_ENTITY, + SIGNAL_REMOVE_HA_DEVICE, + SIGNAL_REMOVE_INSTEON_DEVICE, SIGNAL_REMOVE_X10_DEVICE, SIGNAL_SAVE_DEVICES, SRV_ADD_ALL_LINK, @@ -179,7 +181,7 @@ def register_new_device_callback(hass): @callback -def async_register_services(hass): +def async_register_services(hass): # noqa: C901 """Register services used by insteon component.""" save_lock = asyncio.Lock() @@ -270,14 +272,14 @@ def async_register_services(hass): async def async_add_device_override(override): """Remove an Insten device and associated entities.""" address = Address(override[CONF_ADDRESS]) - await async_remove_device(address) + await async_remove_ha_device(address) devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0) await async_srv_save_devices() async def async_remove_device_override(address): """Remove an Insten device and associated entities.""" address = Address(address) - await async_remove_device(address) + await async_remove_ha_device(address) devices.set_id(address, None, None, None) await devices.async_identify_device(address) await async_srv_save_devices() @@ -304,9 +306,9 @@ def async_register_services(hass): """Remove an X10 device and associated entities.""" address = create_x10_address(housecode, unitcode) devices.pop(address) - await async_remove_device(address) + await async_remove_ha_device(address) - async def async_remove_device(address): + async def async_remove_ha_device(address: Address, remove_all_refs: bool = False): """Remove the device and all entities from hass.""" signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}" async_dispatcher_send(hass, signal) @@ -315,6 +317,15 @@ def async_register_services(hass): if device: dev_registry.async_remove_device(device.id) + async def async_remove_insteon_device( + address: Address, remove_all_refs: bool = False + ): + """Remove the underlying Insteon device from the network.""" + await devices.async_remove_device( + address=address, force=False, remove_all_refs=remove_all_refs + ) + await async_srv_save_devices() + hass.services.async_register( DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA ) @@ -368,6 +379,10 @@ def async_register_services(hass): ) async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device) async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device) + async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device) + async_dispatcher_connect( + hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device + ) _LOGGER.debug("Insteon Services registered") diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 62a0dbdec78..65e967d2af7 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -28,19 +28,21 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( - condition, config_validation as cv, device_registry as dr, entity_registry as er, ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -97,57 +99,72 @@ class _IntegrationMethod(ABC): return _NAME_TO_INTEGRATION_METHOD[method_name]() @abstractmethod - def validate_states(self, left: State, right: State) -> bool: + def validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: """Check state requirements for integration.""" @abstractmethod def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> Decimal: """Calculate area given two states.""" def calculate_area_with_one_state( - self, elapsed_time: float, constant_state: State + self, elapsed_time: Decimal, constant_state: Decimal ) -> Decimal: - return Decimal(constant_state.state) * Decimal(elapsed_time) + return constant_state * elapsed_time class _Trapezoidal(_IntegrationMethod): def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> Decimal: - return Decimal(elapsed_time) * (Decimal(left.state) + Decimal(right.state)) / 2 + return elapsed_time * (left + right) / 2 - def validate_states(self, left: State, right: State) -> bool: - return _is_numeric_state(left) and _is_numeric_state(right) + def validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left.state)) is None or ( + right_dec := _decimal_state(right.state) + ) is None: + return None + return (left_dec, right_dec) class _Left(_IntegrationMethod): def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> 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) + def validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left.state)) is None: + return None + return (left_dec, left_dec) class _Right(_IntegrationMethod): def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> 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 validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: + if (right_dec := _decimal_state(right.state)) is None: + return None + return (right_dec, right_dec) -def _is_numeric_state(state: State) -> bool: +def _decimal_state(state: str) -> Decimal | None: try: - float(state.state) - return True - except (ValueError, TypeError): - return False + return Decimal(state) + except (InvalidOperation, TypeError): + return None _NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { @@ -413,7 +430,7 @@ class IntegrationSensor(RestoreSensor): if old_state is None or new_state is None: return - if condition.state(self.hass, new_state, [STATE_UNAVAILABLE]): + if new_state.state == STATE_UNAVAILABLE: self._attr_available = False self.async_write_ha_state() return @@ -421,18 +438,16 @@ class IntegrationSensor(RestoreSensor): self._attr_available = True self._derive_and_set_attributes_from_state(new_state) - if not self._method.validate_states(old_state, new_state): + if not (states := 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 + elapsed_seconds = Decimal( + (new_state.last_updated - old_state.last_updated).total_seconds() ) + area = self._method.calculate_area_with_two_states(elapsed_seconds, *states) + self._update_integral(area) self.async_write_ha_state() @@ -451,12 +466,10 @@ class IntegrationSensor(RestoreSensor): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes of the sensor.""" - state_attr = { + return { ATTR_SOURCE_ID: self._source_entity, } - return state_attr - @property def extra_restore_state_data(self) -> IntegrationSensorExtraStoredData: """Return sensor specific state data to be restored.""" diff --git a/homeassistant/components/intellifire/number.py b/homeassistant/components/intellifire/number.py index dca7a74c78e..17ed3b7bd27 100644 --- a/homeassistant/components/intellifire/number.py +++ b/homeassistant/components/intellifire/number.py @@ -61,8 +61,7 @@ class IntellifireFlameControlEntity(IntellifireEntity, NumberEntity): def native_value(self) -> float | None: """Return the current Flame Height segment number value.""" # UI uses 1-5 for flame height, backing lib uses 0-4 - value = self.coordinator.read_api.data.flameheight + 1 - return value + return self.coordinator.read_api.data.flameheight + 1 async def async_set_native_value(self, value: float) -> None: """Slider change.""" diff --git a/homeassistant/components/iotawatt/coordinator.py b/homeassistant/components/iotawatt/coordinator.py index e741c7a5a27..4f9ac1f94b7 100644 --- a/homeassistant/components/iotawatt/coordinator.py +++ b/homeassistant/components/iotawatt/coordinator.py @@ -63,6 +63,7 @@ class IotawattUpdater(DataUpdateCoordinator): self.entry.data.get(CONF_USERNAME), self.entry.data.get(CONF_PASSWORD), integratedInterval="d", + includeNonTotalSensors=False, ) try: is_authenticated = await api.connect() diff --git a/homeassistant/components/iotawatt/manifest.json b/homeassistant/components/iotawatt/manifest.json index 5beaa1e318c..5fd178389d9 100644 --- a/homeassistant/components/iotawatt/manifest.json +++ b/homeassistant/components/iotawatt/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/iotawatt", "iot_class": "local_polling", "loggers": ["iotawattpy"], - "requirements": ["ha-iotawattpy==0.1.1"] + "requirements": ["ha-iotawattpy==0.1.2"] } diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index 36e70243c93..a0ecf1f582e 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -40,8 +40,8 @@ class IpmaFlowHandler(ConfigFlow, domain=DOMAIN): user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE], ) - except IPMAException as err: - _LOGGER.exception(err) + except IPMAException: + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry(title=location.name, data=user_input) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 1aad6ae6b21..8d3b97d0ca5 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -120,7 +120,10 @@ async def async_setup_entry( ATTR_MARKER_TYPE: marker.marker_type, }, ), - value_fn=_get_marker_value_fn(index, lambda marker: marker.level), + value_fn=_get_marker_value_fn( + index, + lambda marker: marker.level if marker.level >= 0 else None, + ), ), ) ) diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 12730c9be08..2db89183499 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from prayer_times_calculator import InvalidResponseError, PrayerTimesCalculator -from requests.exceptions import ConnectionError as ConnError import voluptuous as vol from homeassistant.config_entries import ( @@ -15,7 +13,7 @@ from homeassistant.config_entries import ( OptionsFlow, ) from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.helpers.selector import ( LocationSelector, SelectSelector, @@ -23,7 +21,6 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, TextSelector, ) -import homeassistant.util.dt as dt_util from .const import ( CALC_METHODS, @@ -43,26 +40,6 @@ from .const import ( ) -async def async_validate_location( - hass: HomeAssistant, lat: float, lon: float -) -> dict[str, str]: - """Check if the selected location is valid.""" - errors = {} - calc = PrayerTimesCalculator( - latitude=lat, - longitude=lon, - calculation_method=DEFAULT_CALC_METHOD, - date=str(dt_util.now().date()), - ) - try: - await hass.async_add_executor_job(calc.fetch_prayer_times) - except InvalidResponseError: - errors["base"] = "invalid_location" - except ConnError: - errors["base"] = "conn_error" - return errors - - class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): """Handle the Islamic Prayer config flow.""" @@ -81,7 +58,6 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - errors = {} if user_input is not None: lat: float = user_input[CONF_LOCATION][CONF_LATITUDE] @@ -89,14 +65,13 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(f"{lat}-{lon}") self._abort_if_unique_id_configured() - if not (errors := await async_validate_location(self.hass, lat, lon)): - return self.async_create_entry( - title=user_input[CONF_NAME], - data={ - CONF_LATITUDE: lat, - CONF_LONGITUDE: lon, - }, - ) + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_LATITUDE: lat, + CONF_LONGITUDE: lon, + }, + ) home_location = { CONF_LATITUDE: self.hass.config.latitude, @@ -112,7 +87,6 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): ): LocationSelector(), } ), - errors=errors, ) diff --git a/homeassistant/components/islamic_prayer_times/coordinator.py b/homeassistant/components/islamic_prayer_times/coordinator.py index d70d0e2f4fe..7005bee3585 100644 --- a/homeassistant/components/islamic_prayer_times/coordinator.py +++ b/homeassistant/components/islamic_prayer_times/coordinator.py @@ -2,18 +2,17 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import logging from typing import Any, cast -from prayer_times_calculator import PrayerTimesCalculator, exceptions -from requests.exceptions import ConnectionError as ConnError +from prayer_times_calculator_offline import PrayerTimesCalculator from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.event import async_call_later, async_track_point_in_time -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.event import async_track_point_in_time +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( @@ -71,8 +70,8 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim """Return the school.""" return self.config_entry.options.get(CONF_SCHOOL, DEFAULT_SCHOOL) - def get_new_prayer_times(self) -> dict[str, Any]: - """Fetch prayer times for today.""" + def get_new_prayer_times(self, for_date: date) -> dict[str, Any]: + """Fetch prayer times for the specified date.""" calc = PrayerTimesCalculator( latitude=self.latitude, longitude=self.longitude, @@ -80,7 +79,7 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim latitudeAdjustmentMethod=self.lat_adj_method, midnightMode=self.midnight_mode, school=self.school, - date=str(dt_util.now().date()), + date=str(for_date), iso8601=True, ) return cast(dict[str, Any], calc.fetch_prayer_times()) @@ -89,51 +88,18 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim def async_schedule_future_update(self, midnight_dt: datetime) -> None: """Schedule future update for sensors. - Midnight is a calculated time. The specifics of the calculation - depends on the method of the prayer time calculation. This calculated - midnight is the time at which the time to pray the Isha prayers have - expired. + The least surprising behaviour is to load the next day's prayer times only + after the current day's prayers are complete. We will take the fiqhi opinion + that Isha should be prayed before Islamic midnight (which may be before or after 12:00 midnight), + and thus we will switch to the next day's timings at Islamic midnight. - Calculated Midnight: The Islamic midnight. - Traditional Midnight: 12:00AM - - Update logic for prayer times: - - If the Calculated Midnight is before the traditional midnight then wait - until the traditional midnight to run the update. This way the day - will have changed over and we don't need to do any fancy calculations. - - If the Calculated Midnight is after the traditional midnight, then wait - until after the calculated Midnight. We don't want to update the prayer - times too early or else the timings might be incorrect. - - Example: - calculated midnight = 11:23PM (before traditional midnight) - Update time: 12:00AM - - calculated midnight = 1:35AM (after traditional midnight) - update time: 1:36AM. + The +1s is to ensure that any automations predicated on the arrival of Islamic midnight will run. """ _LOGGER.debug("Scheduling next update for Islamic prayer times") - now = dt_util.utcnow() - - if now > midnight_dt: - next_update_at = midnight_dt + timedelta(days=1, minutes=1) - _LOGGER.debug( - "Midnight is after the day changes so schedule update for after Midnight the next day" - ) - else: - _LOGGER.debug( - "Midnight is before the day changes so schedule update for the next start of day" - ) - next_update_at = dt_util.start_of_local_day(now + timedelta(days=1)) - - _LOGGER.debug("Next update scheduled for: %s", next_update_at) - self.event_unsub = async_track_point_in_time( - self.hass, self.async_request_update, next_update_at + self.hass, self.async_request_update, midnight_dt + timedelta(seconds=1) ) async def async_request_update(self, _: datetime) -> None: @@ -141,14 +107,34 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim await self.async_request_refresh() async def _async_update_data(self) -> dict[str, datetime]: - """Update sensors with new prayer times.""" - try: - prayer_times = await self.hass.async_add_executor_job( - self.get_new_prayer_times - ) - except (exceptions.InvalidResponseError, ConnError) as err: - async_call_later(self.hass, 60, self.async_request_update) - raise UpdateFailed from err + """Update sensors with new prayer times. + + Prayer time calculations "roll over" at 12:00 midnight - but this does not mean that all prayers + occur within that Gregorian calendar day. For instance Jasper, Alta. sees Isha occur after 00:00 in the summer. + It is similarly possible (albeit less likely) that Fajr occurs before 00:00. + + As such, to ensure that no prayer times are "unreachable" (e.g. we always see the Isha timestamp pass before loading the next day's times), + we calculate 3 days' worth of times (-1, 0, +1 days) and select the appropriate set based on Islamic midnight. + + The calculation is inexpensive, so there is no need to cache it. + """ + + # Zero out the us component to maintain consistent rollover at T+1s + now = dt_util.now().replace(microsecond=0) + yesterday_times = self.get_new_prayer_times((now - timedelta(days=1)).date()) + today_times = self.get_new_prayer_times(now.date()) + tomorrow_times = self.get_new_prayer_times((now + timedelta(days=1)).date()) + + if ( + yesterday_midnight := dt_util.parse_datetime(yesterday_times["Midnight"]) + ) and now <= yesterday_midnight: + prayer_times = yesterday_times + elif ( + tomorrow_midnight := dt_util.parse_datetime(today_times["Midnight"]) + ) and now > tomorrow_midnight: + prayer_times = tomorrow_times + else: + prayer_times = today_times # introduced in prayer-times-calculator 0.0.8 prayer_times.pop("date", None) diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 5f7e52dd3db..cae3d31feb2 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -1,10 +1,10 @@ { "domain": "islamic_prayer_times", "name": "Islamic Prayer Times", - "codeowners": ["@engrbm87"], + "codeowners": ["@engrbm87", "@cpfair"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", - "iot_class": "cloud_polling", + "iot_class": "calculated", "loggers": ["prayer_times_calculator"], - "requirements": ["prayer-times-calculator==0.0.12"] + "requirements": ["prayer-times-calculator-offline==1.0.3"] } diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index db72cc45a30..0c238182849 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -166,9 +166,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update) ) # Register Integration-wide Services: diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 8b6a4249931..3686a182fe9 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -431,7 +431,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None: def convert_isy_value_to_hass( - value: int | float | None, + value: float | None, uom: str | None, precision: int | str, fallback_precision: int | None = None, diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 69701534840..b9b269d9ca3 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -114,8 +114,5 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return - if ( - ATTR_LAST_BRIGHTNESS in last_state.attributes - and last_state.attributes[ATTR_LAST_BRIGHTNESS] - ): - self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] + if last_brightness := last_state.attributes.get(ATTR_LAST_BRIGHTNESS): + self._last_brightness = last_brightness diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index c24f06d7b19..de9fa805f02 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -73,6 +73,6 @@ async def async_remove_config_entry_device( return not device_entry.identifiers.intersection( ( (DOMAIN, coordinator.server_id), - *((DOMAIN, id) for id in coordinator.device_ids), + *((DOMAIN, device_id) for device_id in coordinator.device_ids), ) ) diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index c6e447d18e8..9a1e3d5985c 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -66,9 +66,9 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" - _LOGGER.exception(ex) + _LOGGER.exception("Unexpected exception") else: entry_title = user_input[CONF_URL] @@ -116,9 +116,9 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" - _LOGGER.exception(ex) + _LOGGER.exception("Unexpected exception") else: self.hass.config_entries.async_update_entry(self.entry, data=new_input) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index add04d1a1ec..6d982458378 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -286,7 +286,7 @@ class JellyfinSource(MediaSource): mime_type = _media_mime_type(track) thumbnail_url = self._get_thumbnail_url(track) - result = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=track_id, media_class=MediaClass.TRACK, @@ -297,8 +297,6 @@ class JellyfinSource(MediaSource): thumbnail=thumbnail_url, ) - return result - async def _build_movie_library( self, library: dict[str, Any], include_children: bool ) -> BrowseMediaSource: @@ -347,7 +345,7 @@ class JellyfinSource(MediaSource): mime_type = _media_mime_type(movie) thumbnail_url = self._get_thumbnail_url(movie) - result = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=movie_id, media_class=MediaClass.MOVIE, @@ -358,8 +356,6 @@ class JellyfinSource(MediaSource): thumbnail=thumbnail_url, ) - return result - async def _build_tv_library( self, library: dict[str, Any], include_children: bool ) -> BrowseMediaSource: @@ -486,7 +482,7 @@ class JellyfinSource(MediaSource): mime_type = _media_mime_type(episode) thumbnail_url = self._get_thumbnail_url(episode) - result = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=episode_id, media_class=MediaClass.EPISODE, @@ -497,8 +493,6 @@ class JellyfinSource(MediaSource): thumbnail=thumbnail_url, ) - return result - async def _get_children( self, parent_id: str, item_type: str ) -> list[dict[str, Any]]: diff --git a/homeassistant/components/jvc_projector/__init__.py b/homeassistant/components/jvc_projector/__init__.py index 28e4cc995bb..8ce1fb46e3d 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, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SELECT, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/jvc_projector/icons.json b/homeassistant/components/jvc_projector/icons.json index c70ded78cb4..a0404b328e1 100644 --- a/homeassistant/components/jvc_projector/icons.json +++ b/homeassistant/components/jvc_projector/icons.json @@ -8,6 +8,11 @@ } } }, + "select": { + "input": { + "default": "mdi:hdmi-port" + } + }, "sensor": { "jvc_power_status": { "default": "mdi:power-plug-off", diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index de7e77197f2..d3e1bf3d940 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.0.9"] + "requirements": ["pyjvcprojector==1.0.11"] } diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index dcc9e5cff51..b69d3b0118b 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Iterable import logging from typing import Any @@ -74,11 +75,13 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.device.power_on() + await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.device.power_off() + await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: diff --git a/homeassistant/components/jvc_projector/select.py b/homeassistant/components/jvc_projector/select.py new file mode 100644 index 00000000000..1395637fad1 --- /dev/null +++ b/homeassistant/components/jvc_projector/select.py @@ -0,0 +1,77 @@ +"""Select platform for the jvc_projector integration.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Final + +from jvcprojector import JvcProjector, const + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import JvcProjectorDataUpdateCoordinator +from .const import DOMAIN +from .entity import JvcProjectorEntity + + +@dataclass(frozen=True, kw_only=True) +class JvcProjectorSelectDescription(SelectEntityDescription): + """Describes JVC Projector select entities.""" + + command: Callable[[JvcProjector, str], Awaitable[None]] + + +OPTIONS: Final[dict[str, dict[str, str]]] = { + "input": {const.HDMI1: const.REMOTE_HDMI_1, const.HDMI2: const.REMOTE_HDMI_2} +} + +SELECTS: Final[list[JvcProjectorSelectDescription]] = [ + JvcProjectorSelectDescription( + key="input", + translation_key="input", + options=list(OPTIONS["input"]), + command=lambda device, option: device.remote(OPTIONS["input"][option]), + ) +] + + +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( + JvcProjectorSelectEntity(coordinator, description) for description in SELECTS + ) + + +class JvcProjectorSelectEntity(JvcProjectorEntity, SelectEntity): + """Representation of a JVC Projector select entity.""" + + entity_description: JvcProjectorSelectDescription + + def __init__( + self, + coordinator: JvcProjectorDataUpdateCoordinator, + description: JvcProjectorSelectDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self.coordinator.data[self.entity_description.key] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.entity_description.command(self.coordinator.device, option) diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index 9991fa1cf67..b89139cbab3 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -38,6 +38,15 @@ "name": "[%key:component::sensor::entity_component::power::name%]" } }, + "select": { + "input": { + "name": "Input", + "state": { + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2" + } + } + }, "sensor": { "jvc_power_status": { "name": "Power status", diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index d7d33dabd44..04ecd633d70 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -79,12 +79,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_ip_mode(host): """Get the 'mode' used to retrieve the MAC address.""" try: - if ipaddress.ip_address(host).version == 6: - return "ip6" - return "ip" + ip_address = ipaddress.ip_address(host) except ValueError: return "hostname" + if ip_address.version == 6: + return "ip6" + return "ip" + async def async_setup_platform( hass: HomeAssistant, diff --git a/homeassistant/components/kegtron/strings.json b/homeassistant/components/kegtron/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/kegtron/strings.json +++ b/homeassistant/components/kegtron/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index 6b6694c920d..94dfca77410 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -32,6 +32,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ Platform.IMAGE, Platform.LAWN_MOWER, Platform.LOCK, + Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH, Platform.WEATHER, @@ -70,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -def _create_issues(hass): +def _create_issues(hass: HomeAssistant) -> None: """Create some issue registry issues.""" async_create_issue( hass, diff --git a/homeassistant/components/kitchen_sink/notify.py b/homeassistant/components/kitchen_sink/notify.py new file mode 100644 index 00000000000..b0418411145 --- /dev/null +++ b/homeassistant/components/kitchen_sink/notify.py @@ -0,0 +1,54 @@ +"""Demo platform that offers a fake notify entity.""" + +from __future__ import annotations + +from homeassistant.components import persistent_notification +from homeassistant.components.notify import NotifyEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the demo notify entity platform.""" + async_add_entities( + [ + DemoNotify( + unique_id="just_notify_me", + device_name="MyBox", + entity_name="Personal notifier", + ), + ] + ) + + +class DemoNotify(NotifyEntity): + """Representation of a demo notify entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + device_name: str, + entity_name: str | None, + ) -> None: + """Initialize the Demo button entity.""" + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) + self._attr_name = entity_name + + async def async_send_message(self, message: str) -> None: + """Send out a persistent notification.""" + persistent_notification.async_create(self.hass, message, "Demo notification") diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c84d53d6039..da68dc36a6d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -197,11 +197,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: [ platform for platform in SUPPORTED_PLATFORMS - if platform in config and platform not in (Platform.SENSOR, Platform.NOTIFY) + if platform in config and platform is not Platform.SENSOR ], ) - # set up notify platform, no entry support for notify component yet + # set up notify service for backwards compatibility - remove 2024.11 if NotifySchema.PLATFORM in config: hass.async_create_task( discovery.async_load_platform( @@ -232,7 +232,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform for platform in SUPPORTED_PLATFORMS if platform in hass.data[DATA_KNX_CONFIG] - and platform not in (Platform.SENSOR, Platform.NOTIFY) + and platform is not Platform.SENSOR ], ], ) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 6e4a3b80f6e..12343f0dca7 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -18,11 +18,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, - async_track_state_change_event, + HomeAssistant, + State, + callback, ) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, StateType from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS diff --git a/homeassistant/components/knx/helpers/__init__.py b/homeassistant/components/knx/helpers/__init__.py new file mode 100644 index 00000000000..25d84406d03 --- /dev/null +++ b/homeassistant/components/knx/helpers/__init__.py @@ -0,0 +1 @@ +"""Helpers for KNX.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index af0c6b8d01c..77f3db3f9f3 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "config_flow": true, - "dependencies": ["file_upload", "websocket_api"], + "dependencies": ["file_upload", "repairs", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/knx", "integration_type": "hub", "iot_class": "local_push", diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 74ae86dc5d0..e208e4fd646 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,4 +1,4 @@ -"""Support for KNX/IP notification services.""" +"""Support for KNX/IP notifications.""" from __future__ import annotations @@ -7,13 +7,16 @@ from typing import Any from xknx import XKNX from xknx.devices import Notification as XknxNotification -from homeassistant.components.notify import BaseNotificationService -from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant import config_entries +from homeassistant.components.notify import BaseNotificationService, NotifyEntity +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .schema import NotifySchema +from .knx_entity import KnxEntity +from .repairs import migrate_notify_issue async def async_get_service( @@ -25,16 +28,11 @@ async def async_get_service( if discovery_info is None: return None - if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM): + if platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.NOTIFY): xknx: XKNX = hass.data[DOMAIN].xknx notification_devices = [ - XknxNotification( - xknx, - name=device_config[CONF_NAME], - group_address=device_config[KNX_ADDRESS], - value_type=device_config[CONF_TYPE], - ) + _create_notification_instance(xknx, device_config) for device_config in platform_config ] return KNXNotificationService(notification_devices) @@ -59,6 +57,7 @@ class KNXNotificationService(BaseNotificationService): async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a notification to knx bus.""" + migrate_notify_issue(self.hass) if "target" in kwargs: await self._async_send_to_device(message, kwargs["target"]) else: @@ -74,3 +73,41 @@ class KNXNotificationService(BaseNotificationService): for device in self.devices: if device.name in names: await device.set(message) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up notify(s) for KNX platform.""" + xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY] + + async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config) + + +def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification: + """Return a KNX Notification to be used within XKNX.""" + return XknxNotification( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + value_type=config[CONF_TYPE], + ) + + +class KNXNotify(NotifyEntity, KnxEntity): + """Representation of a KNX notification entity.""" + + _device: XknxNotification + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX notification.""" + super().__init__(_create_notification_instance(xknx, config)) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = str(self._device.remote_value.group_address) + + async def async_send_message(self, message: str) -> None: + """Send a notification to knx bus.""" + await self._device.set(message) diff --git a/homeassistant/components/knx/repairs.py b/homeassistant/components/knx/repairs.py new file mode 100644 index 00000000000..f0a92850d36 --- /dev/null +++ b/homeassistant/components/knx/repairs.py @@ -0,0 +1,36 @@ +"""Repairs support for KNX.""" + +from __future__ import annotations + +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir + +from .const import DOMAIN + + +@callback +def migrate_notify_issue(hass: HomeAssistant) -> None: + """Create issue for notify service deprecation.""" + ir.async_create_issue( + hass, + DOMAIN, + "migrate_notify", + breaks_in_ha_version="2024.11.0", + issue_domain=Platform.NOTIFY.value, + is_fixable=True, + is_persistent=True, + translation_key="migrate_notify", + severity=ir.IssueSeverity.WARNING, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert issue_id == "migrate_notify" + return ConfirmRepairFlow() diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 39670b4f92b..462605c3985 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -750,6 +750,7 @@ class NotifySchema(KNXPlatformSchema): vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, vol.Required(KNX_ADDRESS): ga_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 39b96dddf8f..a69ba106ffd 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -384,5 +384,18 @@ "name": "[%key:common::action::reload%]", "description": "Reloads the KNX integration." } + }, + "issues": { + "migrate_notify": { + "title": "Migration of KNX notify service", + "fix_flow": { + "step": { + "confirm": { + "description": "The KNX `notify` service has been migrated. New `notify` entities are available now.\n\nUpdate any automations to use the new `notify.send_message` exposed by these new entities. When this is done, fix this issue and restart Home Assistant.", + "title": "Disable legacy KNX notify service" + } + } + } + } } } diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 9fe87a2c3f6..4e56314a677 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -37,17 +37,17 @@ string_type_validator = dpt_subclass_validator(DPTString) def ga_validator(value: Any) -> str | int: """Validate that value is parsable as GroupAddress or InternalGroupAddress.""" - if isinstance(value, (str, int)): - try: - parse_device_group_address(value) - return value - except CouldNotParseAddress as exc: - raise vol.Invalid( - f"'{value}' is not a valid KNX group address: {exc.message}" - ) from exc - raise vol.Invalid( - f"'{value}' is not a valid KNX group address: Invalid type '{type(value).__name__}'" - ) + if not isinstance(value, (str, int)): + raise vol.Invalid( + f"'{value}' is not a valid KNX group address: Invalid type '{type(value).__name__}'" + ) + try: + parse_device_group_address(value) + except CouldNotParseAddress as exc: + raise vol.Invalid( + f"'{value}' is not a valid KNX group address: {exc.message}" + ) from exc + return value ga_list_validator = vol.All( diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 1d9d1ca4f7c..b4d9c575122 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -300,7 +300,7 @@ class KodiConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _get_data(self): - data = { + return { CONF_NAME: self._name, CONF_HOST: self._host, CONF_PORT: self._port, @@ -311,8 +311,6 @@ class KodiConfigFlow(ConfigFlow, domain=DOMAIN): CONF_TIMEOUT: DEFAULT_TIMEOUT, } - return data - class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 4a4e6539f03..37666557eff 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -235,8 +235,7 @@ class SettingDataUpdateCoordinator( _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) - fetched_data = await client.get_setting_values(self._fetch) - return fetched_data + return await client.get_setting_values(self._fetch) class PlenticoreSelectUpdateCoordinator(DataUpdateCoordinator[_DataT]): # pylint: disable=hass-enforce-coordinator-module @@ -295,9 +294,7 @@ class SelectDataUpdateCoordinator( _LOGGER.debug("Fetching select %s for %s", self.name, self._fetch) - fetched_data = await self._async_get_current_option(self._fetch) - - return fetched_data + return await self._async_get_current_option(self._fetch) async def _async_get_current_option( self, @@ -313,8 +310,7 @@ class SelectDataUpdateCoordinator( continue for option in val.values(): if option[all_option] == "1": - fetched = {mid: {cast(str, pids[0]): all_option}} - return fetched + return {mid: {cast(str, pids[0]): all_option}} return {mid: {cast(str, pids[0]): "None"}} return {} diff --git a/homeassistant/components/kraken/utils.py b/homeassistant/components/kraken/utils.py index 210756a7792..ec89d1b1584 100644 --- a/homeassistant/components/kraken/utils.py +++ b/homeassistant/components/kraken/utils.py @@ -9,7 +9,9 @@ def get_tradable_asset_pairs(kraken_api: KrakenAPI) -> dict[str, str]: """Get a list of tradable asset pairs.""" tradable_asset_pairs = {} asset_pairs_df = kraken_api.get_tradable_asset_pairs() - for pair in zip(asset_pairs_df.index.values, asset_pairs_df["wsname"]): + for pair in zip( + asset_pairs_df.index.values, asset_pairs_df["wsname"], strict=False + ): # Remove darkpools # https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool if not pair[0].endswith(".d"): diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index ed1477e1149..f21b0cb0a3c 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -147,8 +147,8 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self._async_step_create_entry( host, user_input[CONF_API_KEY] ) - except AbortFlow as ex: - raise ex + except AbortFlow: + raise except LaMetricConnectionError as ex: LOGGER.error("Error connecting to LaMetric: %s", ex) errors["base"] = "cannot_connect" @@ -209,8 +209,8 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self._async_step_create_entry( str(device.ip), device.api_key ) - except AbortFlow as ex: - raise ex + except AbortFlow: + raise except LaMetricConnectionError as ex: LOGGER.error("Error connecting to LaMetric: %s", ex) errors["base"] = "cannot_connect" diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py index f28bd1308b0..8cb9850bde7 100644 --- a/homeassistant/components/lawn_mower/__init__.py +++ b/homeassistant/components/lawn_mower/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -25,12 +26,6 @@ from .const import ( LawnMowerEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index ca07dbe0ef6..49b54fc0c8d 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -299,7 +299,9 @@ class SendKeys(LcnServiceCall): keys = [[False] * 8 for i in range(4)] - key_strings = zip(service.data[CONF_KEYS][::2], service.data[CONF_KEYS][1::2]) + key_strings = zip( + service.data[CONF_KEYS][::2], service.data[CONF_KEYS][1::2], strict=False + ) for table, key in key_strings: table_id = ord(table) - 65 diff --git a/homeassistant/components/leaone/strings.json b/homeassistant/components/leaone/strings.json index 6391c754dec..bb684941147 100644 --- a/homeassistant/components/leaone/strings.json +++ b/homeassistant/components/leaone/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/lg_netcast/__init__.py b/homeassistant/components/lg_netcast/__init__.py index 232d7bd10b8..f6fb834ab11 100644 --- a/homeassistant/components/lg_netcast/__init__.py +++ b/homeassistant/components/lg_netcast/__init__.py @@ -1 +1,33 @@ """The lg_netcast component.""" + +from typing import Final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER] + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + + return unload_ok diff --git a/homeassistant/components/lg_netcast/config_flow.py b/homeassistant/components/lg_netcast/config_flow.py new file mode 100644 index 00000000000..3c1d3d73e0f --- /dev/null +++ b/homeassistant/components/lg_netcast/config_flow.py @@ -0,0 +1,217 @@ +"""Config flow to configure the LG Netcast TV integration.""" + +from __future__ import annotations + +import contextlib +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Any + +from pylgnetcast import AccessTokenError, LgNetCastClient, SessionIdError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) +from homeassistant.core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, callback +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.util.network import is_host_valid + +from .const import DEFAULT_NAME, DOMAIN +from .helpers import LGNetCastDetailDiscoveryError, async_discover_netcast_details + +DISPLAY_ACCESS_TOKEN_INTERVAL = timedelta(seconds=1) + + +class LGNetCast(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LG Netcast TV integration.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize config flow.""" + self.client: LgNetCastClient | None = None + self.device_config: dict[str, Any] = {} + self._discovered_devices: dict[str, Any] = {} + self._track_interval: CALLBACK_TYPE | None = None + + def create_client(self) -> None: + """Create LG Netcast client from config.""" + host = self.device_config[CONF_HOST] + access_token = self.device_config.get(CONF_ACCESS_TOKEN) + self.client = LgNetCastClient(host, access_token) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + + if user_input is not None: + host = user_input[CONF_HOST] + if is_host_valid(host): + self.device_config[CONF_HOST] = host + return await self.async_step_authorize() + + errors[CONF_HOST] = "invalid_host" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: + """Import configuration from yaml.""" + self.device_config = { + CONF_HOST: config[CONF_HOST], + CONF_NAME: config[CONF_NAME], + } + + def _create_issue(): + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LG Netcast", + }, + ) + + try: + result: ConfigFlowResult = await self.async_step_authorize(config) + except AbortFlow as err: + if err.reason != "already_configured": + async_create_issue( + self.hass, + DOMAIN, + "deprecated_yaml_import_issue_{err.reason}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{err.reason}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LG Netcast", + "error_type": err.reason, + }, + ) + else: + _create_issue() + raise + + _create_issue() + + return result + + async def async_discover_client(self): + """Handle Discovery step.""" + self.create_client() + + if TYPE_CHECKING: + assert self.client is not None + + if self.device_config.get(CONF_ID): + return + + try: + details = await async_discover_netcast_details(self.hass, self.client) + except LGNetCastDetailDiscoveryError as err: + raise AbortFlow("cannot_connect") from err + + if (unique_id := details["uuid"]) is None: + raise AbortFlow("invalid_host") + + self.device_config[CONF_ID] = unique_id + self.device_config[CONF_MODEL] = details["model_name"] + + if CONF_NAME not in self.device_config: + self.device_config[CONF_NAME] = details["friendly_name"] or DEFAULT_NAME + + async def async_step_authorize( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle Authorize step.""" + errors: dict[str, str] = {} + self.async_stop_display_access_token() + + if user_input is not None and user_input.get(CONF_ACCESS_TOKEN) is not None: + self.device_config[CONF_ACCESS_TOKEN] = user_input[CONF_ACCESS_TOKEN] + + await self.async_discover_client() + assert self.client is not None + + await self.async_set_unique_id(self.device_config[CONF_ID]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.device_config[CONF_HOST]} + ) + + try: + await self.hass.async_add_executor_job( + self.client._get_session_id # pylint: disable=protected-access + ) + except AccessTokenError: + if user_input is not None: + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + except SessionIdError: + errors["base"] = "cannot_connect" + else: + return await self.async_create_device() + + self._track_interval = async_track_time_interval( + self.hass, + self.async_display_access_token, + DISPLAY_ACCESS_TOKEN_INTERVAL, + cancel_on_shutdown=True, + ) + + return self.async_show_form( + step_id="authorize", + data_schema=vol.Schema( + { + vol.Optional(CONF_ACCESS_TOKEN): vol.All(str, vol.Length(max=6)), + } + ), + errors=errors, + ) + + async def async_display_access_token(self, _: datetime | None = None): + """Display access token on screen.""" + assert self.client is not None + with contextlib.suppress(AccessTokenError, SessionIdError): + await self.hass.async_add_executor_job( + self.client._get_session_id # pylint: disable=protected-access + ) + + @callback + def async_remove(self): + """Terminate Access token display if flow is removed.""" + self.async_stop_display_access_token() + + def async_stop_display_access_token(self): + """Stop Access token request if running.""" + if self._track_interval is not None: + self._track_interval() + self._track_interval = None + + async def async_create_device(self) -> ConfigFlowResult: + """Create LG Netcast TV Device from config.""" + assert self.client + + return self.async_create_entry( + title=self.device_config[CONF_NAME], data=self.device_config + ) diff --git a/homeassistant/components/lg_netcast/const.py b/homeassistant/components/lg_netcast/const.py index 0344ad6f177..aca01c9b870 100644 --- a/homeassistant/components/lg_netcast/const.py +++ b/homeassistant/components/lg_netcast/const.py @@ -1,3 +1,9 @@ """Constants for the lg_netcast component.""" +from typing import Final + +ATTR_MANUFACTURER: Final = "LG" + +DEFAULT_NAME: Final = "LG Netcast TV" + DOMAIN = "lg_netcast" diff --git a/homeassistant/components/lg_netcast/device_trigger.py b/homeassistant/components/lg_netcast/device_trigger.py new file mode 100644 index 00000000000..51c5ec53004 --- /dev/null +++ b/homeassistant/components/lg_netcast/device_trigger.py @@ -0,0 +1,88 @@ +"""Provides device triggers for LG Netcast.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.typing import ConfigType + +from . import trigger +from .const import DOMAIN +from .helpers import async_get_device_entry_by_device_id +from .triggers.turn_on import ( + PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE, + async_get_turn_on_trigger, +) + +TRIGGER_TYPES = {TURN_ON_PLATFORM_TYPE} + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == TURN_ON_PLATFORM_TYPE: + device_id = config[CONF_DEVICE_ID] + + try: + device = async_get_device_entry_by_device_id(hass, device_id) + except ValueError as err: + raise InvalidDeviceAutomationConfig(err) from err + + if DOMAIN in hass.data: + for config_entry_id in device.config_entries: + if hass.data[DOMAIN].get(config_entry_id): + break + else: + raise InvalidDeviceAutomationConfig( + f"Device {device.id} is not from an existing {DOMAIN} config entry" + ) + + return config + + +async def async_get_triggers( + _hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List device triggers for LG Netcast devices.""" + return [async_get_turn_on_trigger(device_id)] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + if (trigger_type := config[CONF_TYPE]) == TURN_ON_PLATFORM_TYPE: + trigger_config = { + CONF_PLATFORM: trigger_type, + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + } + trigger_config = await trigger.async_validate_trigger_config( + hass, trigger_config + ) + return await trigger.async_attach_trigger( + hass, trigger_config, action, trigger_info + ) + + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/lg_netcast/helpers.py b/homeassistant/components/lg_netcast/helpers.py new file mode 100644 index 00000000000..7cfc0d50271 --- /dev/null +++ b/homeassistant/components/lg_netcast/helpers.py @@ -0,0 +1,59 @@ +"""Helper functions for LG Netcast TV.""" + +from typing import TypedDict +import xml.etree.ElementTree as ET + +from pylgnetcast import LgNetCastClient +from requests import RequestException + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN + + +class LGNetCastDetailDiscoveryError(Exception): + """Unable to retrieve details from Netcast TV.""" + + +class NetcastDetails(TypedDict): + """Netcast TV Details.""" + + uuid: str + model_name: str + friendly_name: str + + +async def async_discover_netcast_details( + hass: HomeAssistant, client: LgNetCastClient +) -> NetcastDetails: + """Discover UUID and Model Name from Netcast Tv.""" + try: + resp = await hass.async_add_executor_job(client.query_device_info) + except RequestException as err: + raise LGNetCastDetailDiscoveryError( + f"Error in connecting to {client.url}" + ) from err + except ET.ParseError as err: + raise LGNetCastDetailDiscoveryError("Invalid XML") from err + + if resp is None: + raise LGNetCastDetailDiscoveryError("Empty response received") + + return resp + + +@callback +def async_get_device_entry_by_device_id( + hass: HomeAssistant, device_id: str +) -> DeviceEntry: + """Get Device Entry from Device Registry by device ID. + + Raises ValueError if device ID is invalid. + """ + device_reg = dr.async_get(hass) + if (device := device_reg.async_get(device_id)) is None: + raise ValueError(f"Device {device_id} is not a valid {DOMAIN} device.") + + return device diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 8a63e064b41..cf91374feb7 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -1,9 +1,12 @@ { "domain": "lg_netcast", "name": "LG Netcast", - "codeowners": ["@Drafteed"], + "codeowners": ["@Drafteed", "@splinter98"], + "config_flow": true, + "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/lg_netcast", + "integration_type": "device", "iot_class": "local_polling", "loggers": ["pylgnetcast"], - "requirements": ["pylgnetcast==0.3.7"] + "requirements": ["pylgnetcast==0.3.9"] } diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 81927710299..3fc07cab12b 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError from requests import RequestException @@ -17,14 +17,19 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script +from homeassistant.helpers.trigger import PluggableAction from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import ATTR_MANUFACTURER, DOMAIN +from .triggers.turn_on import async_get_turn_on_trigger DEFAULT_NAME = "LG TV Remote" @@ -54,23 +59,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a LG Netcast Media Player from a config_entry.""" + + host = config_entry.data[CONF_HOST] + access_token = config_entry.data[CONF_ACCESS_TOKEN] + unique_id = config_entry.unique_id + name = config_entry.data.get(CONF_NAME, DEFAULT_NAME) + model = config_entry.data[CONF_MODEL] + + client = LgNetCastClient(host, access_token) + + hass.data[DOMAIN][config_entry.entry_id] = client + + async_add_entities([LgTVDevice(client, name, model, unique_id=unique_id)]) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the LG TV platform.""" host = config.get(CONF_HOST) - access_token = config.get(CONF_ACCESS_TOKEN) - name = config[CONF_NAME] - on_action = config.get(CONF_ON_ACTION) - client = LgNetCastClient(host, access_token) - on_action_script = Script(hass, on_action, name, DOMAIN) if on_action else None + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) - add_entities([LgTVDevice(client, name, on_action_script)], True) + if ( + result.get("type") == FlowResultType.ABORT + and result.get("reason") == "cannot_connect" + ): + raise PlatformNotReady(f"Connection error while connecting to {host}") class LgTVDevice(MediaPlayerEntity): @@ -79,19 +106,42 @@ class LgTVDevice(MediaPlayerEntity): _attr_assumed_state = True _attr_device_class = MediaPlayerDeviceClass.TV _attr_media_content_type = MediaType.CHANNEL + _attr_has_entity_name = True + _attr_name = None - def __init__(self, client, name, on_action_script): + def __init__(self, client, name, model, unique_id): """Initialize the LG TV device.""" self._client = client - self._name = name self._muted = False - self._on_action_script = on_action_script + self._turn_on = PluggableAction(self.async_write_ha_state) self._volume = 0 self._channel_id = None self._channel_name = "" self._program_name = "" self._sources = {} self._source_names = [] + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + name=name, + model=model, + ) + + async def async_added_to_hass(self) -> None: + """Connect and subscribe to dispatcher signals and state updates.""" + await super().async_added_to_hass() + + entry = self.registry_entry + + if TYPE_CHECKING: + assert entry is not None and entry.device_id is not None + + self.async_on_remove( + self._turn_on.async_register( + self.hass, async_get_turn_on_trigger(entry.device_id) + ) + ) def send_command(self, command): """Send remote control commands to the TV.""" @@ -131,7 +181,7 @@ class LgTVDevice(MediaPlayerEntity): channel_name = channel.find("chname") if channel_name is not None: channel_names.append(str(channel_name.text)) - self._sources = dict(zip(channel_names, channel_list)) + self._sources = dict(zip(channel_names, channel_list, strict=False)) # sort source names by the major channel number source_tuples = [ (k, source.find("major").text) @@ -151,11 +201,6 @@ class LgTVDevice(MediaPlayerEntity): self._volume = volume self._muted = muted - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def is_volume_muted(self): """Boolean if volume is currently muted.""" @@ -194,7 +239,7 @@ class LgTVDevice(MediaPlayerEntity): @property def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - if self._on_action_script: + if self._turn_on: return SUPPORT_LGTV | MediaPlayerEntityFeature.TURN_ON return SUPPORT_LGTV @@ -209,10 +254,9 @@ class LgTVDevice(MediaPlayerEntity): """Turn off media player.""" self.send_command(LG_COMMAND.POWER) - def turn_on(self) -> None: + async def async_turn_on(self) -> None: """Turn on the media player.""" - if self._on_action_script: - self._on_action_script.run(context=self._context) + await self._turn_on.async_run(self.hass, self._context) def volume_up(self) -> None: """Volume up the media player.""" diff --git a/homeassistant/components/lg_netcast/strings.json b/homeassistant/components/lg_netcast/strings.json new file mode 100644 index 00000000000..77003f60f43 --- /dev/null +++ b/homeassistant/components/lg_netcast/strings.json @@ -0,0 +1,46 @@ +{ + "config": { + "step": { + "user": { + "description": "Ensure that your TV is turned on before trying to set it up.\nIf you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of the LG Netcast TV to control." + } + }, + "authorize": { + "title": "Authorize LG Netcast TV", + "description": "Enter the Pairing Key displayed on the TV", + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The {integration_title} is not online for YAML migration to complete", + "description": "Migrating {integration_title} from YAML cannot complete until the TV is online.\n\nPlease turn on your TV for migration to complete." + }, + "deprecated_yaml_import_issue_invalid_host": { + "title": "The {integration_title} YAML configuration has an invalid host.", + "description": "Configuring {integration_title} using YAML is being removed but the device returned an invalid response.\n\nPlease check or manually remove the YAML configuration." + } + }, + "device_automation": { + "trigger_type": { + "lg_netcast.turn_on": "Device is requested to turn on" + } + } +} diff --git a/homeassistant/components/lg_netcast/trigger.py b/homeassistant/components/lg_netcast/trigger.py new file mode 100644 index 00000000000..8dfbe309e03 --- /dev/null +++ b/homeassistant/components/lg_netcast/trigger.py @@ -0,0 +1,49 @@ +"""LG Netcast TV trigger dispatcher.""" + +from __future__ import annotations + +from typing import cast + +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import ( + TriggerActionType, + TriggerInfo, + TriggerProtocol, +) +from homeassistant.helpers.typing import ConfigType + +from .triggers import turn_on + +TRIGGERS = { + "turn_on": turn_on, +} + + +def _get_trigger_platform(config: ConfigType) -> TriggerProtocol: + """Return trigger platform.""" + platform_split = config[CONF_PLATFORM].split(".", maxsplit=1) + if len(platform_split) < 2 or platform_split[1] not in TRIGGERS: + raise ValueError( + f"Unknown LG Netcast TV trigger platform {config[CONF_PLATFORM]}" + ) + return cast(TriggerProtocol, TRIGGERS[platform_split[1]]) + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + platform = _get_trigger_platform(config) + return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach trigger of specified platform.""" + platform = _get_trigger_platform(config) + return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/lg_netcast/triggers/__init__.py b/homeassistant/components/lg_netcast/triggers/__init__.py new file mode 100644 index 00000000000..d352620118e --- /dev/null +++ b/homeassistant/components/lg_netcast/triggers/__init__.py @@ -0,0 +1 @@ +"""LG Netcast triggers.""" diff --git a/homeassistant/components/lg_netcast/triggers/turn_on.py b/homeassistant/components/lg_netcast/triggers/turn_on.py new file mode 100644 index 00000000000..118ed89797e --- /dev/null +++ b/homeassistant/components/lg_netcast/triggers/turn_on.py @@ -0,0 +1,115 @@ +"""LG Netcast TV device turn on trigger.""" + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.trigger import ( + PluggableAction, + TriggerActionType, + TriggerInfo, +) +from homeassistant.helpers.typing import ConfigType + +from ..const import DOMAIN +from ..helpers import async_get_device_entry_by_device_id + +PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" + +TRIGGER_SCHEMA = vol.All( + cv.TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): PLATFORM_TYPE, + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + }, + ), + cv.has_at_least_one_key(ATTR_ENTITY_ID, ATTR_DEVICE_ID), +) + + +def async_get_turn_on_trigger(device_id: str) -> dict[str, str]: + """Return data for a turn on trigger.""" + + return { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: PLATFORM_TYPE, + } + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, + *, + platform_type: str = PLATFORM_TYPE, +) -> CALLBACK_TYPE | None: + """Attach a trigger.""" + device_ids = set() + if ATTR_DEVICE_ID in config: + device_ids.update(config.get(ATTR_DEVICE_ID, [])) + + if ATTR_ENTITY_ID in config: + ent_reg = er.async_get(hass) + + def _get_device_id_from_entity_id(entity_id): + entity_entry = ent_reg.async_get(entity_id) + + if ( + entity_entry is None + or entity_entry.device_id is None + or entity_entry.platform != DOMAIN + ): + raise ValueError(f"Entity {entity_id} is not a valid {DOMAIN} entity.") + + return entity_entry.device_id + + device_ids.update( + { + _get_device_id_from_entity_id(entity_id) + for entity_id in config.get(ATTR_ENTITY_ID, []) + } + ) + + trigger_data = trigger_info["trigger_data"] + + unsubs = [] + + for device_id in device_ids: + device = async_get_device_entry_by_device_id(hass, device_id) + device_name = device.name_by_user or device.name + + variables = { + **trigger_data, + CONF_PLATFORM: platform_type, + ATTR_DEVICE_ID: device_id, + "description": f"lg netcast turn on trigger for {device_name}", + } + + turn_on_trigger = async_get_turn_on_trigger(device_id) + + unsubs.append( + PluggableAction.async_attach_trigger( + hass, turn_on_trigger, action, {"trigger": variables} + ) + ) + + @callback + def async_remove() -> None: + """Remove state listeners async.""" + for unsub in unsubs: + unsub() + unsubs.clear() + + return async_remove diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index cd1ce1c8139..250cba887c1 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -35,5 +35,4 @@ async def async_unload_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload a config entry.""" - result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return result + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 39412780331..6aa7fdc6305 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -16,9 +16,11 @@ "homekit": { "models": [ "LIFX A19", + "LIFX A21", "LIFX Beam", "LIFX BR30", "LIFX Candle", + "LIFX Ceiling", "LIFX Clean", "LIFX Color", "LIFX DLCOL", @@ -27,12 +29,16 @@ "LIFX Downlight", "LIFX Filament", "LIFX GU10", + "LIFX Indoor Neon", "LIFX Lightstrip", "LIFX Mini", "LIFX Neon", "LIFX Nightvision", + "LIFX PAR38", "LIFX Pls", "LIFX Plus", + "LIFX Round", + "LIFX Square", "LIFX String", "LIFX Tile", "LIFX White", @@ -42,8 +48,8 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.0", + "aiolifx==1.0.2", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.4.10" + "aiolifx-themes==0.4.15" ] } diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index bb77c7595d3..9782fe4adba 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -149,7 +149,7 @@ def merge_hsbk( Hue, Saturation, Brightness, Kelvin """ - return [b if c is None else c for b, c in zip(base, change)] + return [b if c is None else c for b, c in zip(base, change, strict=False)] def _get_mac_offset(mac_addr: str, offset: int) -> str: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 53c7328ece4..b3b1330b3a1 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -7,9 +7,10 @@ import csv import dataclasses from datetime import timedelta from enum import IntFlag, StrEnum +from functools import cached_property import logging import os -from typing import TYPE_CHECKING, Any, Self, cast, final +from typing import Any, Self, cast, final import voluptuous as vol @@ -34,11 +35,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) DATA_PROFILES = "light_profiles" @@ -521,13 +517,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_COLOR_TEMP_KELVIN] ) elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes: - assert (rgb_color := params.pop(ATTR_RGB_COLOR)) is not None + rgb_color = params.pop(ATTR_RGB_COLOR) + assert rgb_color is not None if ColorMode.RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: - # https://github.com/python/mypy/issues/13673 params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( - *rgb_color, # type: ignore[call-arg] + *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin, ) @@ -588,9 +584,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ( ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes ): - assert (rgbww_color := params.pop(ATTR_RGBWW_COLOR)) is not None - # https://github.com/python/mypy/issues/13673 - rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] + rgbww_color = params.pop(ATTR_RGBWW_COLOR) + assert rgbww_color is not None + rgb_color = color_util.color_rgbww_to_rgb( *rgbww_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) if ColorMode.RGB in supported_color_modes: @@ -961,8 +957,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @property def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" - rgbw_color = self.rgbw_color - return rgbw_color + return self.rgbw_color @cached_property def rgbww_color(self) -> tuple[int, int, int, int, int] | None: diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py index bfb6f825030..31629f8e3b0 100644 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ b/homeassistant/components/linear_garage_door/config_flow.py @@ -53,15 +53,13 @@ async def validate_input( finally: await hub.close() - info = { + return { "email": data["email"], "password": data["password"], "sites": sites, "device_id": device_id, } - return info - class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Linear Garage Door.""" diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 39a1646a8b7..aada2f6c9cb 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -94,7 +94,7 @@ class LitterRobotConfigFlow(ConfigFlow, domain=DOMAIN): return "invalid_auth" except LitterRobotException: return "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown" return "" diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 4af004bddf5..2e31bcf9906 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -55,7 +55,6 @@ class LitterRobotHub: load_robots=load_robots, subscribe_for_updates=subscribe_for_updates, ) - return except LitterRobotLoginException as ex: raise ConfigEntryAuthFailed("Invalid credentials") from ex except LitterRobotException as ex: diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 66ade5f356c..88396f9f9c1 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.11"] + "requirements": ["pylitterbot==2023.5.0"] } diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index 5b25abf8e21..ccd3d8db759 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -18,6 +18,7 @@ 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.setup import SetupPhases, async_pause_setup from homeassistant.util import dt as dt_util from .const import CONF_TODO_LIST_NAME, DOMAIN @@ -67,9 +68,16 @@ async def async_setup_entry( ) -> None: """Set up the local_todo todo platform.""" - store = hass.data[DOMAIN][config_entry.entry_id] + store: LocalTodoListStore = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() - calendar = IcsCalendarStream.calendar_from_ics(ics) + + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # calendar_from_ics will dynamically load packages + # the first time it is called, so we need to do it + # in a separate thread to avoid blocking the event loop + calendar: Calendar = await hass.async_add_import_executor_job( + IcsCalendarStream.calendar_from_ics, ics + ) migrated = _migrate_calendar(calendar) calendar.prodid = PRODID diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b2cd28324cb..10c1526c5bb 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging import re from typing import TYPE_CHECKING, Any, final @@ -44,11 +45,6 @@ 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: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) ATTR_CHANGED_BY = "changed_by" diff --git a/homeassistant/components/lock/icons.json b/homeassistant/components/lock/icons.json index 1bf48f2ab40..0ce2e70d372 100644 --- a/homeassistant/components/lock/icons.json +++ b/homeassistant/components/lock/icons.json @@ -5,7 +5,7 @@ "state": { "jammed": "mdi:lock-alert", "locking": "mdi:lock-clock", - "unlocked": "mdi:lock-open", + "unlocked": "mdi:lock-open-variant", "unlocking": "mdi:lock-clock" } } @@ -13,6 +13,6 @@ "services": { "lock": "mdi:lock", "open": "mdi:door-open", - "unlock": "mdi:lock-open" + "unlock": "mdi:lock-open-variant" } } diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index f19e64aa6f0..d520cafb80e 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.integration_platform import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.event_type import EventType from . import rest_api, websocket_api from .const import ( # noqa: F401 @@ -134,7 +135,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entities_filter = None external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + EventType[Any] | str, + tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]], ] = {} hass.data[DOMAIN] = LogbookConfig(external_events, filters, entities_filter) websocket_api.async_setup(hass) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 1731fcaddd9..4fa0da9033a 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -17,6 +17,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -24,10 +25,8 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.util.event_type import EventType from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LogbookConfig @@ -65,7 +64,7 @@ def _async_config_entries_for_ids( def async_determine_event_types( hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None -) -> tuple[str, ...]: +) -> tuple[EventType[Any] | str, ...]: """Reduce the event types based on the entity ids and device ids.""" logbook_config: LogbookConfig = hass.data[DOMAIN] external_events = logbook_config.external_events @@ -83,7 +82,7 @@ def async_determine_event_types( # to add them since we have historically included # them when matching only on entities # - intrested_event_types: set[str] = { + intrested_event_types: set[EventType[Any] | str] = { external_event for external_event, domain_call in external_events.items() if domain_call[0] in interested_domains @@ -162,7 +161,7 @@ def async_subscribe_events( hass: HomeAssistant, subscriptions: list[CALLBACK_TYPE], target: Callable[[Event[Any]], None], - event_types: tuple[str, ...], + event_types: tuple[EventType[Any] | str, ...], entities_filter: Callable[[str], bool] | None, entity_ids: list[str] | None, device_ids: list[str] | None, @@ -177,8 +176,7 @@ def async_subscribe_events( target, entities_filter, entity_ids, device_ids ) subscriptions.extend( - hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) - for event_type in event_types + hass.bus.async_listen(event_type, event_forwarder) for event_type in event_types ) if device_ids and not entity_ids: @@ -212,7 +210,6 @@ def async_subscribe_events( hass.bus.async_listen( EVENT_STATE_CHANGED, _forward_state_events_filtered, - run_immediately=True, ) ) diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 1073c6b0d3a..2f9b2c8e289 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass +from functools import cached_property from typing import TYPE_CHECKING, Any, cast from sqlalchemy.engine.row import Row @@ -17,21 +18,18 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED from homeassistant.core import Context, Event, State, callback +from homeassistant.util.event_type import EventType from homeassistant.util.json import json_loads from homeassistant.util.ulid import ulid_to_bytes -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - @dataclass(slots=True) class LogbookConfig: """Configuration for the logbook integration.""" external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + EventType[Any] | str, + tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]], ] sqlalchemy_filter: Filters | None = None entity_filter: Callable[[str], bool] | None = None @@ -70,7 +68,7 @@ class LazyEventPartialState: ) @cached_property - def event_type(self) -> str | None: + def event_type(self) -> EventType[Any] | str | None: """Return the event type.""" return self.row.event_type @@ -114,7 +112,7 @@ class EventAsRow: icon: str | None = None context_user_id_bin: bytes | None = None context_parent_id_bin: bytes | None = None - event_type: str | None = None + event_type: EventType[Any] | str | None = None state: str | None = None context_only: None = None diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 2180a63b74f..df1eb6a15f2 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util +from homeassistant.util.event_type import EventType from .const import ( ATTR_MESSAGE, @@ -75,7 +76,8 @@ class LogbookRun: context_lookup: dict[bytes | None, Row | EventAsRow | None] external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + EventType[Any] | str, + tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]], ] event_cache: EventCache entity_name_cache: EntityNameCache @@ -90,7 +92,7 @@ class EventProcessor: def __init__( self, hass: HomeAssistant, - event_types: tuple[str, ...], + event_types: tuple[EventType[Any] | str, ...], entity_ids: list[str] | None = None, device_ids: list[str] | None = None, context_id: str | None = None, diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index f4b1c06c40c..0e67ad23381 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -82,7 +82,7 @@ def devices_stmt( json_quotable_device_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple devices.""" - stmt = lambda_stmt( + return lambda_stmt( lambda: _apply_devices_context_union( select_events_without_states(start_day, end_day, event_type_ids).where( apply_event_device_id_matchers(json_quotable_device_ids) @@ -93,7 +93,6 @@ def devices_stmt( json_quotable_device_ids, ).order_by(Events.time_fired_ts) ) - return stmt def apply_event_device_id_matchers( diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 383bb71e223..bef34f0858b 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -110,7 +110,7 @@ def entities_devices_stmt( json_quoted_device_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" - stmt = lambda_stmt( + return lambda_stmt( lambda: _apply_entities_devices_context_union( select_events_without_states(start_day, end_day, event_type_ids).where( _apply_event_entity_id_device_id_matchers( @@ -125,7 +125,6 @@ def entities_devices_stmt( json_quoted_device_ids, ).order_by(Events.time_fired_ts) ) - return stmt def _apply_event_entity_id_device_id_matchers( diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index 6357510a07e..8ddf4a1a543 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -49,8 +49,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: payload = {"host": le_wh, "event": json_body} requests.post(le_wh, data=json.dumps(payload), timeout=10) - except requests.exceptions.RequestException as error: - _LOGGER.exception("Error sending to Logentries: %s", error) + except requests.exceptions.RequestException: + _LOGGER.exception("Error sending to Logentries") hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener) diff --git a/homeassistant/components/logger/helpers.py b/homeassistant/components/logger/helpers.py index a527a081fca..034266428a3 100644 --- a/homeassistant/components/logger/helpers.py +++ b/homeassistant/components/logger/helpers.py @@ -28,6 +28,16 @@ from .const import ( STORAGE_VERSION, ) +SAVE_DELAY = 15.0 +# At startup, we want to save after a long delay to avoid +# saving while the system is still starting up. If the system +# for some reason restarts quickly, it will still be written +# at the final write event. In most cases we expect startup +# to happen in less than 180 seconds, but if it takes longer +# it's likely delayed because of remote I/O and not local +# I/O so it's fine to save at that point. +SAVE_DELAY_LONG = 180.0 + @callback def async_get_domain_config(hass: HomeAssistant) -> LoggerDomainConfig: @@ -148,7 +158,7 @@ class LoggerSettings: for domain, settings in stored_log_config.items() } } - await self._store.async_save(self._async_data_to_save()) + self.async_save(SAVE_DELAY_LONG) @callback def _async_data_to_save(self) -> dict[str, dict[str, dict[str, str]]]: @@ -164,9 +174,9 @@ class LoggerSettings: } @callback - def async_save(self) -> None: + def async_save(self, delay: float = SAVE_DELAY) -> None: """Save settings.""" - self._store.async_delay_save(self._async_data_to_save, 15) + self._store.async_delay_save(self._async_data_to_save, delay) @callback def _async_get_logger_logs(self) -> dict[str, int]: diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index d62c1b07b5c..183f383e7e4 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -71,11 +71,10 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next( + return next( (result.hostname for result in self.last_results if result.mac == device), None, ) - return name def get_extra_attributes(self, device): """Get extra attributes of a device. diff --git a/homeassistant/components/lupusec/config_flow.py b/homeassistant/components/lupusec/config_flow.py index c3fe7295266..3af823e4fa1 100644 --- a/homeassistant/components/lupusec/config_flow.py +++ b/homeassistant/components/lupusec/config_flow.py @@ -7,7 +7,7 @@ from typing import Any import lupupy import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_IP_ADDRESS, @@ -31,12 +31,12 @@ DATA_SCHEMA = vol.Schema( ) -class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LupusecConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Lupusec config flow.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -66,9 +66,7 @@ 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] - ) -> config_entries.ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" self._async_abort_entries_match( { diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 517eb4c8350..828182547c2 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -3,31 +3,25 @@ from dataclasses import dataclass import logging -from pylutron import Button, Keypad, Led, Lutron, LutronEvent, OccupancyGroup, Output +from pylutron import Button, Keypad, Led, Lutron, OccupancyGroup, Output import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ID, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - Platform, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify from .const import DOMAIN PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.LIGHT, Platform.SCENE, @@ -105,69 +99,13 @@ async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: return True -class LutronButton: - """Representation of a button on a Lutron keypad. - - This is responsible for firing events as keypad buttons are pressed - (and possibly released, depending on the button type). It is not - represented as an entity; it simply fires events. - """ - - def __init__( - self, hass: HomeAssistant, area_name: str, keypad: Keypad, button: Button - ) -> None: - """Register callback for activity on the button.""" - name = f"{keypad.name}: {button.name}" - if button.name == "Unknown Button": - name += f" {button.number}" - self._hass = hass - self._has_release_event = ( - button.button_type is not None and "RaiseLower" in button.button_type - ) - self._id = slugify(name) - self._keypad = keypad - self._area_name = area_name - self._button_name = button.name - self._button = button - self._event = "lutron_event" - self._full_id = slugify(f"{area_name} {name}") - self._uuid = button.uuid - - button.subscribe(self.button_callback, None) - - def button_callback( - self, _button: Button, _context: None, event: LutronEvent, _params: dict - ) -> None: - """Fire an event about a button being pressed or released.""" - # Events per button type: - # RaiseLower -> pressed/released - # SingleAction -> single - action = None - if self._has_release_event: - if event == Button.Event.PRESSED: - action = "pressed" - else: - action = "released" - elif event == Button.Event.PRESSED: - action = "single" - - if action: - data = { - ATTR_ID: self._id, - ATTR_ACTION: action, - ATTR_FULL_ID: self._full_id, - ATTR_UUID: self._uuid, - } - self._hass.bus.fire(self._event, data) - - @dataclass(slots=True, kw_only=True) class LutronData: """Storage class for platform global data.""" client: Lutron binary_sensors: list[tuple[str, OccupancyGroup]] - buttons: list[LutronButton] + buttons: list[tuple[str, Keypad, Button]] covers: list[tuple[str, Output]] fans: list[tuple[str, Output]] lights: list[tuple[str, Output]] @@ -273,8 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b led.legacy_uuid, entry_data.client.guid, ) - - entry_data.buttons.append(LutronButton(hass, area.name, keypad, button)) + if button.button_type: + entry_data.buttons.append((area.name, keypad, button)) if area.occupancy_group is not None: entry_data.binary_sensors.append((area.name, area.occupancy_group)) platform = Platform.BINARY_SENSOR diff --git a/homeassistant/components/lutron/event.py b/homeassistant/components/lutron/event.py new file mode 100644 index 00000000000..710f942a006 --- /dev/null +++ b/homeassistant/components/lutron/event.py @@ -0,0 +1,109 @@ +"""Support for Lutron events.""" + +from enum import StrEnum + +from pylutron import Button, Keypad, Lutron, LutronEvent + +from homeassistant.components.event import EventEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, DOMAIN, LutronData +from .entity import LutronKeypad + + +class LutronEventType(StrEnum): + """Lutron event types.""" + + SINGLE_PRESS = "single_press" + PRESS = "press" + RELEASE = "release" + + +LEGACY_EVENT_TYPES: dict[LutronEventType, str] = { + LutronEventType.SINGLE_PRESS: "single", + LutronEventType.PRESS: "pressed", + LutronEventType.RELEASE: "released", +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Lutron event platform.""" + entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + LutronEventEntity(area_name, keypad, button, entry_data.client) + for area_name, keypad, button in entry_data.buttons + ) + + +class LutronEventEntity(LutronKeypad, EventEntity): + """Representation of a Lutron keypad button.""" + + _attr_translation_key = "button" + + def __init__( + self, + area_name: str, + keypad: Keypad, + button: Button, + controller: Lutron, + ) -> None: + """Initialize the button.""" + super().__init__(area_name, button, controller, keypad) + if (name := button.name) == "Unknown Button": + name += f" {button.number}" + self._attr_name = name + self._has_release_event = ( + button.button_type is not None and "RaiseLower" in button.button_type + ) + if self._has_release_event: + self._attr_event_types = [LutronEventType.PRESS, LutronEventType.RELEASE] + else: + self._attr_event_types = [LutronEventType.SINGLE_PRESS] + + self._full_id = slugify(f"{area_name} {name}") + self._id = slugify(name) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + self._lutron_device.subscribe(self.handle_event, None) + + async def async_will_remove_from_hass(self) -> None: + """Unregister callbacks.""" + await super().async_will_remove_from_hass() + # Temporary solution until https://github.com/thecynic/pylutron/pull/93 gets merged + self._lutron_device._subscribers.remove((self.handle_event, None)) # pylint: disable=protected-access + + @callback + def handle_event( + self, button: Button, _context: None, event: LutronEvent, _params: dict + ) -> None: + """Handle received event.""" + action: LutronEventType | None = None + if self._has_release_event: + if event == Button.Event.PRESSED: + action = LutronEventType.PRESS + else: + action = LutronEventType.RELEASE + elif event == Button.Event.PRESSED: + action = LutronEventType.SINGLE_PRESS + + if action: + data = { + ATTR_ID: self._id, + ATTR_ACTION: LEGACY_EVENT_TYPES[action], + ATTR_FULL_ID: self._full_id, + ATTR_UUID: button.uuid, + } + self.hass.bus.fire("lutron_event", data) + self._trigger_event(action) + self.async_write_ha_state() diff --git a/homeassistant/components/lutron/strings.json b/homeassistant/components/lutron/strings.json index efa0a35d81a..0212c8845d5 100644 --- a/homeassistant/components/lutron/strings.json +++ b/homeassistant/components/lutron/strings.json @@ -22,6 +22,21 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "entity": { + "event": { + "button": { + "state_attributes": { + "event_type": { + "state": { + "single_press": "Single press", + "press": "Press", + "release": "Release" + } + } + } + } + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The Lutron YAML configuration import cannot connect to server", diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index e2c85c1400b..349e4f871a3 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -12,6 +12,7 @@ from aiolyric import Lyric from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricAccessories, LyricRoom from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -77,7 +78,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(60): await lyric.get_locations() - return lyric + await asyncio.gather( + *( + lyric.get_thermostat_rooms(location.locationID, device.deviceID) + for location in lyric.locations + for device in location.devices + if device.deviceClass == "Thermostat" + ) + ) + except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. # Honeywell appear to have issues keeping tokens saved. @@ -87,6 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from exception except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception + return lyric coordinator = DataUpdateCoordinator[Lyric]( hass, @@ -159,8 +169,43 @@ class LyricDeviceEntity(LyricEntity): def device_info(self) -> DeviceInfo: """Return device information about this Honeywell Lyric instance.""" return DeviceInfo( + identifiers={(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, connections={(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, manufacturer="Honeywell", model=self.device.deviceModel, - name=self.device.name, + name=f"{self.device.name} Thermostat", + ) + + +class LyricAccessoryEntity(LyricDeviceEntity): + """Defines a Honeywell Lyric accessory entity, a sub-device of a thermostat.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Lyric], + location: LyricLocation, + device: LyricDevice, + room: LyricRoom, + accessory: LyricAccessories, + key: str, + ) -> None: + """Initialize the Honeywell Lyric accessory entity.""" + super().__init__(coordinator, location, device, key) + self._room = room + self._accessory = accessory + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Honeywell Lyric instance.""" + return DeviceInfo( + identifiers={ + ( + f"{dr.CONNECTION_NETWORK_MAC}_room_accessory", + f"{self._mac_id}_room{self._room.id}_accessory{self._accessory.id}", + ) + }, + manufacturer="Honeywell", + model="RCHTSENSOR", + name=f"{self._room.roomName} Sensor", + via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id), ) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 276336e02cc..64f60fa6611 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -9,6 +9,7 @@ from datetime import datetime, timedelta from aiolyric import Lyric from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricAccessories, LyricRoom from homeassistant.components.sensor import ( SensorDeviceClass, @@ -24,7 +25,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util -from . import LyricDeviceEntity +from . import LyricAccessoryEntity, LyricDeviceEntity from .const import ( DOMAIN, PRESET_HOLD_UNTIL, @@ -50,6 +51,14 @@ class LyricSensorEntityDescription(SensorEntityDescription): suitable_fn: Callable[[LyricDevice], bool] +@dataclass(frozen=True, kw_only=True) +class LyricSensorAccessoryEntityDescription(SensorEntityDescription): + """Class describing Honeywell Lyric room sensor entities.""" + + value_fn: Callable[[LyricRoom, LyricAccessories], StateType | datetime] + suitable_fn: Callable[[LyricRoom, LyricAccessories], bool] + + DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ LyricSensorEntityDescription( key="indoor_temperature", @@ -109,6 +118,26 @@ DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ ), ] +ACCESSORY_SENSORS: list[LyricSensorAccessoryEntityDescription] = [ + LyricSensorAccessoryEntityDescription( + key="room_temperature", + translation_key="room_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda _, accessory: accessory.temperature, + suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", + ), + LyricSensorAccessoryEntityDescription( + key="room_humidity", + translation_key="room_humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda room, _: room.roomAvgHumidity, + suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", + ), +] + def get_setpoint_status(status: str, time: str) -> str | None: """Get status of the setpoint.""" @@ -147,6 +176,18 @@ async def async_setup_entry( if device_sensor.suitable_fn(device) ) + async_add_entities( + LyricAccessorySensor( + coordinator, accessory_sensor, location, device, room, accessory + ) + for location in coordinator.data.locations + for device in location.devices + for room in coordinator.data.rooms_dict.get(device.macID, {}).values() + for accessory in room.accessories + for accessory_sensor in ACCESSORY_SENSORS + if accessory_sensor.suitable_fn(room, accessory) + ) + class LyricSensor(LyricDeviceEntity, SensorEntity): """Define a Honeywell Lyric sensor.""" @@ -178,3 +219,40 @@ class LyricSensor(LyricDeviceEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return the state.""" return self.entity_description.value_fn(self.device) + + +class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): + """Define a Honeywell Lyric sensor.""" + + entity_description: LyricSensorAccessoryEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator[Lyric], + description: LyricSensorAccessoryEntityDescription, + location: LyricLocation, + parentDevice: LyricDevice, + room: LyricRoom, + accessory: LyricAccessories, + ) -> None: + """Initialize.""" + super().__init__( + coordinator, + location, + parentDevice, + room, + accessory, + f"{parentDevice.macID}_room{room.id}_acc{accessory.id}_{description.key}", + ) + self.room = room + self.entity_description = description + if description.device_class == SensorDeviceClass.TEMPERATURE: + if parentDevice.units == "Fahrenheit": + self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT + else: + self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + + @property + def native_value(self) -> StateType | datetime: + """Return the state.""" + return self.entity_description.value_fn(self._room, self._accessory) diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 68bb6292f9e..739ad7fad68 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -41,6 +41,12 @@ }, "setpoint_status": { "name": "Setpoint status" + }, + "room_temperature": { + "name": "Room temperature" + }, + "room_humidity": { + "name": "Room humidity" } } }, diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index b000f1eadcb..337a58e3b2f 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -110,14 +110,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.register_shutdown() await component.async_add_entities([mailbox_entity]) - setup_tasks = [ - asyncio.create_task(async_setup_platform(p_type, p_config)) - for p_type, p_config in config_per_platform(config, DOMAIN) - if p_type is not None - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) + for p_type, p_config in config_per_platform(config, DOMAIN): + if p_type is not None: + hass.async_create_task( + async_setup_platform(p_type, p_config), eager_start=True + ) async def async_platform_discovered( platform: str, info: DiscoveryInfoType | None diff --git a/homeassistant/components/mailbox/strings.json b/homeassistant/components/mailbox/strings.json index 44f1ad08d39..01746e3e98d 100644 --- a/homeassistant/components/mailbox/strings.json +++ b/homeassistant/components/mailbox/strings.json @@ -3,7 +3,7 @@ "issues": { "deprecated_mailbox": { "title": "The mailbox platform is being removed", - "description": "The mailbox platform is being removed. Please report it to the author of the '{integration_domain}' custom integration." + "description": "The mailbox platform is being removed. Please report it to the author of the \"{integration_domain}\" custom integration." } } } diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index ed5b3a69135..39aea79d15e 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -86,8 +86,8 @@ class MailgunNotificationService(BaseNotificationService): except MailgunCredentialsError: _LOGGER.exception("Invalid credentials") return False - except MailgunDomainError as mailgun_error: - _LOGGER.exception(mailgun_error) + except MailgunDomainError: + _LOGGER.exception("Unexpected exception") return False return True @@ -110,5 +110,5 @@ class MailgunNotificationService(BaseNotificationService): files=files, ) _LOGGER.debug("Message sent: %s", resp) - except MailgunError as mailgun_error: - _LOGGER.exception("Failed to send message: %s", mailgun_error) + except MailgunError: + _LOGGER.exception("Failed to send message") diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 0cd92b552c6..db81825d7b5 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -28,12 +28,11 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index b8f1ec08fe0..4c9af45e63f 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -308,7 +308,9 @@ class MatrixBot: async def _resolve_room_aliases(self, listening_rooms: list[RoomAnyID]) -> None: """Resolve any RoomAliases into RoomIDs for the purpose of client interactions.""" resolved_rooms = [ - self.hass.async_create_task(self._resolve_room_alias(room_alias_or_id)) + self.hass.async_create_task( + self._resolve_room_alias(room_alias_or_id), eager_start=False + ) for room_alias_or_id in listening_rooms ] for resolved_room in asyncio.as_completed(resolved_rooms): @@ -330,7 +332,9 @@ class MatrixBot: async def _join_rooms(self) -> None: """Join the Matrix rooms that we listen for commands in.""" rooms = [ - self.hass.async_create_task(self._join_room(room_id, room_alias_or_id)) + self.hass.async_create_task( + self._join_room(room_id, room_alias_or_id), eager_start=False + ) for room_alias_or_id, room_id in self._listening_rooms.items() ] await asyncio.wait(rooms) @@ -438,7 +442,8 @@ class MatrixBot: target_room=target_room, message_type=message_type, content=content, - ) + ), + eager_start=False, ) for target_room in target_rooms ) @@ -514,7 +519,9 @@ class MatrixBot: and len(target_rooms) > 0 ): image_tasks = [ - self.hass.async_create_task(self._send_image(image_path, target_rooms)) + self.hass.async_create_task( + self._send_image(image_path, target_rooms), eager_start=False + ) for image_path in image_paths ] await asyncio.wait(image_tasks) diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py index 7dc06807a98..b079dcd9b54 100644 --- a/homeassistant/components/matter/config_flow.py +++ b/homeassistant/components/matter/config_flow.py @@ -17,6 +17,8 @@ from homeassistant.components.hassio import ( HassioServiceInfo, is_hassio, ) +from homeassistant.components.onboarding import async_is_onboarded +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant @@ -64,6 +66,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up flow instance.""" + self._running_in_background = False self.ws_address: str | None = None # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False @@ -78,7 +81,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) - if not self.install_task.done(): + if not self._running_in_background and not self.install_task.done(): return self.async_show_progress( step_id="install_addon", progress_action="install_addon", @@ -89,12 +92,16 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): await self.install_task except AddonError as err: LOGGER.error(err) + if self._running_in_background: + return await self.async_step_install_failed() return self.async_show_progress_done(next_step_id="install_failed") finally: self.install_task = None self.integration_created_addon = True + if self._running_in_background: + return await self.async_step_start_addon() return self.async_show_progress_done(next_step_id="start_addon") async def async_step_install_failed( @@ -125,7 +132,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): """Start Matter Server add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) - if not self.start_task.done(): + if not self._running_in_background and not self.start_task.done(): return self.async_show_progress( step_id="start_addon", progress_action="start_addon", @@ -136,10 +143,14 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): await self.start_task except (FailedConnect, AddonError, AbortFlow) as err: LOGGER.error(err) + if self._running_in_background: + return await self.async_step_start_failed() return self.async_show_progress_done(next_step_id="start_failed") finally: self.start_task = None + if self._running_in_background: + return await self.async_step_finish_addon_setup() return self.async_show_progress_done(next_step_id="finish_addon_setup") async def async_step_start_failed( @@ -223,6 +234,18 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=get_manual_schema(user_input), errors=errors ) + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + if not async_is_onboarded(self.hass) and is_hassio(self.hass): + await self._async_handle_discovery_without_unique_id() + self._running_in_background = True + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) + return await self._async_step_discovery_without_unique_id() + async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> ConfigFlowResult: diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index dcb3586934b..a47147e874a 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -107,9 +107,6 @@ class MatterEntity(Entity): attr_path_filter=attr_path, ) ) - await self.matter_client.subscribe_attribute( - self._endpoint.node.node_id, sub_paths - ) # subscribe to node (availability changes) self._unsubscribes.append( self.matter_client.subscribe_events( diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 9aa58879214..cab9b602753 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -109,7 +109,7 @@ def get_node_from_device_entry( if server_info is None: raise RuntimeError("Matter server information is not available") - node = next( + return next( ( node for node in matter_client.get_nodes() @@ -118,5 +118,3 @@ def get_node_from_device_entry( ), None, ) - - return node diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index fce780896a4..9d80ebc38f6 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -43,6 +43,18 @@ COLOR_MODE_MAP = { } DEFAULT_TRANSITION = 0.2 +# there's a bug in (at least) Espressif's implementation of light transitions +# on devices based on Matter 1.0. Mark potential devices with this issue. +# https://github.com/home-assistant/core/issues/113775 +# vendorid (attributeKey 0/40/2) +# productid (attributeKey 0/40/4) +# hw version (attributeKey 0/40/8) +# sw version (attributeKey 0/40/10) +TRANSITION_BLOCKLIST = ( + (4488, 514, "1.0", "1.0.0"), + (5010, 769, "3.0", "1.0.0"), +) + async def async_setup_entry( hass: HomeAssistant, @@ -61,6 +73,7 @@ class MatterLight(MatterEntity, LightEntity): _supports_brightness = False _supports_color = False _supports_color_temperature = False + _transitions_disabled = False async def _set_xy_color( self, xy_color: tuple[float, float], transition: float = 0.0 @@ -260,6 +273,8 @@ class MatterLight(MatterEntity, LightEntity): color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) transition = kwargs.get(ATTR_TRANSITION, DEFAULT_TRANSITION) + if self._transitions_disabled: + transition = 0 if self.supported_color_modes is not None: if hs_color is not None and ColorMode.HS in self.supported_color_modes: @@ -295,7 +310,10 @@ class MatterLight(MatterEntity, LightEntity): # brightness support if self._entity_info.endpoint.has_attribute( None, clusters.LevelControl.Attributes.CurrentLevel - ): + ) and self._entity_info.endpoint.device_types != {device_types.OnOffLight}: + # We need to filter out the OnOffLight device type here because + # that can have an optional LevelControl cluster present + # which we should ignore. supported_color_modes.add(ColorMode.BRIGHTNESS) self._supports_brightness = True # colormode(s) @@ -333,8 +351,12 @@ class MatterLight(MatterEntity, LightEntity): supported_color_modes = filter_supported_color_modes(supported_color_modes) self._attr_supported_color_modes = supported_color_modes + self._check_transition_blocklist() # flag support for transition as soon as we support setting brightness and/or color - if supported_color_modes != {ColorMode.ONOFF}: + if ( + supported_color_modes != {ColorMode.ONOFF} + and not self._transitions_disabled + ): self._attr_supported_features |= LightEntityFeature.TRANSITION LOGGER.debug( @@ -373,6 +395,21 @@ class MatterLight(MatterEntity, LightEntity): else: self._attr_color_mode = ColorMode.ONOFF + def _check_transition_blocklist(self) -> None: + """Check if this device is reported to have non working transitions.""" + device_info = self._endpoint.device_info + if ( + device_info.vendorID, + device_info.productID, + device_info.hardwareVersionString, + device_info.softwareVersionString, + ) in TRANSITION_BLOCKLIST: + self._transitions_disabled = True + LOGGER.warning( + "Detected a device that has been reported to have firmware issues " + "with light transitions. Transitions will be disabled for this light" + ) + # Discovery schema(s) to map Matter Attributes to HA entities DISCOVERY_SCHEMAS = [ @@ -406,11 +443,11 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.CurrentHue, clusters.ColorControl.Attributes.CurrentSaturation, ), optional_attributes=( + clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.ColorTemperatureMireds, clusters.ColorControl.Attributes.ColorMode, clusters.ColorControl.Attributes.CurrentX, @@ -426,11 +463,11 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.CurrentX, clusters.ColorControl.Attributes.CurrentY, ), optional_attributes=( + clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.ColorTemperatureMireds, clusters.ColorControl.Attributes.ColorMode, clusters.ColorControl.Attributes.CurrentHue, @@ -451,36 +488,4 @@ DISCOVERY_SCHEMAS = [ ), optional_attributes=(clusters.ColorControl.Attributes.ColorMode,), ), - # Additional schema to match generic dimmable lights with incorrect/missing device type - MatterDiscoverySchema( - platform=Platform.LIGHT, - entity_description=LightEntityDescription( - key="MatterDimmableLightFallback", name=None - ), - entity_class=MatterLight, - required_attributes=( - clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, - ), - optional_attributes=( - clusters.ColorControl.Attributes.ColorMode, - clusters.ColorControl.Attributes.CurrentHue, - clusters.ColorControl.Attributes.CurrentSaturation, - clusters.ColorControl.Attributes.CurrentX, - clusters.ColorControl.Attributes.CurrentY, - clusters.ColorControl.Attributes.ColorTemperatureMireds, - ), - # important: make sure to rule out all device types that are also based on the - # onoff and levelcontrol clusters ! - not_device_type=( - device_types.Fan, - device_types.GenericSwitch, - device_types.OnOffPlugInUnit, - device_types.HeatingCoolingUnit, - device_types.Pump, - device_types.CastingVideoClient, - device_types.VideoRemoteControl, - device_types.Speaker, - ), - ), ] diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 716e296ec15..20988e387fe 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==5.7.0"] + "requirements": ["python-matter-server==5.10.0"], + "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 9bc858d40c0..f148102cfcd 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -81,12 +81,8 @@ DISCOVERY_SCHEMAS = [ device_types.ColorTemperatureLight, device_types.DimmableLight, device_types.ExtendedColorLight, - device_types.OnOffLight, - device_types.DoorLock, device_types.ColorDimmerSwitch, - device_types.DimmerSwitch, - device_types.Thermostat, - device_types.RoomAirConditioner, + device_types.OnOffLight, ), ), ] diff --git a/homeassistant/components/medcom_ble/config_flow.py b/homeassistant/components/medcom_ble/config_flow.py index faf482ca1f9..a50a5876cc7 100644 --- a/homeassistant/components/medcom_ble/config_flow.py +++ b/homeassistant/components/medcom_ble/config_flow.py @@ -136,11 +136,10 @@ class InspectorBLEConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") except AbortFlow: raise - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error occurred reading information from %s: %s", + "Error occurred reading information from %s", self._discovery_info.address, - err, ) return self.async_abort(reason="unknown") _LOGGER.debug("Device connection successful, proceeding") diff --git a/homeassistant/components/medcom_ble/strings.json b/homeassistant/components/medcom_ble/strings.json index 56cfb5a1dd7..4f2b29b7269 100644 --- a/homeassistant/components/medcom_ble/strings.json +++ b/homeassistant/components/medcom_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 888265e8d3c..56b768c26a2 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -17,18 +17,29 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType +from .const import ( + ATTR_FORMAT_QUERY, + ATTR_URL, + DEFAULT_STREAM_QUERY, + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, +) + _LOGGER = logging.getLogger(__name__) CONF_CUSTOMIZE_ENTITIES = "customize" CONF_DEFAULT_STREAM_QUERY = "default_query" -DEFAULT_STREAM_QUERY = "best" -DOMAIN = "media_extractor" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -44,14 +55,66 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media extractor service.""" + async def extract_media_url(call: ServiceCall) -> ServiceResponse: + """Extract media url.""" + youtube_dl = YoutubeDL( + {"quiet": True, "logger": _LOGGER, "format": call.data[ATTR_FORMAT_QUERY]} + ) + + def extract_info() -> dict[str, Any]: + return cast( + dict[str, Any], + youtube_dl.extract_info( + call.data[ATTR_URL], download=False, process=False + ), + ) + + result = await hass.async_add_executor_job(extract_info) + if "entries" in result: + _LOGGER.warning("Playlists are not supported, looking for the first video") + entries = list(result["entries"]) + if entries: + selected_media = entries[0] + else: + raise HomeAssistantError("Playlist is empty") + else: + selected_media = result + if "formats" in selected_media: + if selected_media["extractor"] == "youtube": + url = get_best_stream_youtube(selected_media["formats"]) + else: + url = get_best_stream(selected_media["formats"]) + else: + url = cast(str, selected_media["url"]) + return {"url": url} + def play_media(call: ServiceCall) -> None: """Get stream URL and send it to the play_media service.""" MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() - hass.services.register( + default_format_query = config.get(DOMAIN, {}).get( + CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY + ) + + hass.services.async_register( + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, + extract_media_url, + schema=vol.Schema( + { + vol.Required(ATTR_URL): cv.string, + vol.Optional( + ATTR_FORMAT_QUERY, default=default_format_query + ): cv.string, + } + ), + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( DOMAIN, SERVICE_PLAY_MEDIA, play_media, @@ -215,9 +278,9 @@ def get_best_stream_youtube(formats: list[dict[str, Any]]) -> str: return get_best_stream( [ - format - for format in formats - if format.get("acodec", "none") != "none" - and format.get("vcodec", "none") != "none" + stream_format + for stream_format in formats + if stream_format.get("acodec", "none") != "none" + and stream_format.get("vcodec", "none") != "none" ] ) diff --git a/homeassistant/components/media_extractor/const.py b/homeassistant/components/media_extractor/const.py new file mode 100644 index 00000000000..009ab37602c --- /dev/null +++ b/homeassistant/components/media_extractor/const.py @@ -0,0 +1,9 @@ +"""Constants for media_extractor.""" + +DEFAULT_STREAM_QUERY = "best" +DOMAIN = "media_extractor" + +ATTR_URL = "url" +ATTR_FORMAT_QUERY = "format_query" + +SERVICE_EXTRACT_MEDIA_URL = "extract_media_url" diff --git a/homeassistant/components/media_extractor/icons.json b/homeassistant/components/media_extractor/icons.json index 71b65e7c4a6..7abc4410b19 100644 --- a/homeassistant/components/media_extractor/icons.json +++ b/homeassistant/components/media_extractor/icons.json @@ -1,5 +1,6 @@ { "services": { - "play_media": "mdi:play" + "play_media": "mdi:play", + "extract_media_url": "mdi:link" } } diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index 8af2d12d0e9..abfe52dc4f5 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -19,3 +19,14 @@ play_media: - "MUSIC" - "TVSHOW" - "VIDEO" +extract_media_url: + fields: + url: + required: true + example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + selector: + text: + format_query: + example: "best" + selector: + text: diff --git a/homeassistant/components/media_extractor/strings.json b/homeassistant/components/media_extractor/strings.json index 0cdffd5d508..1af84b5b8c8 100644 --- a/homeassistant/components/media_extractor/strings.json +++ b/homeassistant/components/media_extractor/strings.json @@ -13,6 +13,20 @@ "description": "The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC." } } + }, + "extract_media_url": { + "name": "Get Media URL", + "description": "Extract media url from a service.", + "fields": { + "url": { + "name": "Media URL", + "description": "URL where the media can be found." + }, + "format_query": { + "name": "Format query", + "description": "Youtube-dl query to select the quality of the result." + } + } } } } diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 6535aea3e52..35e1b1cb71e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -9,12 +9,12 @@ from contextlib import suppress import datetime as dt from enum import StrEnum import functools as ft -from functools import lru_cache +from functools import cached_property, lru_cache import hashlib from http import HTTPStatus import logging import secrets -from typing import TYPE_CHECKING, Any, Final, Required, TypedDict, final +from typing import Any, Final, Required, TypedDict, final from urllib.parse import quote, urlparse from aiohttp import web @@ -134,11 +134,6 @@ from .const import ( # noqa: F401 ) from .errors import BrowseError -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 8a67ae4a5b4..a1685df285e 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -93,12 +93,10 @@ class LocalSource(MediaSource): else: source_dir_id, location = None, "" - result = await self.hass.async_add_executor_job( + return await self.hass.async_add_executor_job( self._browse_media, source_dir_id, location ) - return result - def _browse_media( self, source_dir_id: str | None, location: str ) -> BrowseMediaSource: diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 4bf12650b82..08b3658c270 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -330,12 +330,11 @@ class AtwDeviceZoneClimate(MelCloudClimate): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes with device specific additions.""" - data = { + return { ATTR_STATUS: ATW_ZONE_HVAC_MODE_LOOKUP.get( self._zone.status, self._zone.status ) } - return data @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 7d170430b04..8de1ac53311 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -73,8 +73,7 @@ class AtwWaterHeater(WaterHeaterEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes with device specific additions.""" - data = {ATTR_STATUS: self._device.status} - return data + return {ATTR_STATUS: self._device.status} @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 1e729258218..f81d60c3d00 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -25,9 +25,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = await hass.async_add_executor_job( meteoclimatic_client.weather_at_station, station_code ) - return data.__dict__ except MeteoclimaticError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err + return data.__dict__ coordinator = DataUpdateCoordinator( hass, @@ -49,5 +49,4 @@ 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) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/microbees/config_flow.py b/homeassistant/components/microbees/config_flow.py index dedb6c1f374..c54f8939145 100644 --- a/homeassistant/components/microbees/config_flow.py +++ b/homeassistant/components/microbees/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from microBeesPy import MicroBees, MicroBeesException from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow @@ -32,9 +33,7 @@ class OAuth2FlowHandler( scopes = ["read", "write"] return {"scope": " ".join(scopes)} - async def async_oauth_create_entry( - self, data: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" microbees = MicroBees( @@ -65,7 +64,7 @@ class OAuth2FlowHandler( async def async_step_reauth( self, entry_data: Mapping[str, Any] - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -74,7 +73,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") diff --git a/homeassistant/components/microbees/cover.py b/homeassistant/components/microbees/cover.py index bdf6e815af1..b6d5d366d89 100644 --- a/homeassistant/components/microbees/cover.py +++ b/homeassistant/components/microbees/cover.py @@ -19,6 +19,8 @@ from .const import DOMAIN from .coordinator import MicroBeesUpdateCoordinator from .entity import MicroBeesEntity +COVER_IDS = {47: "roller_shutter"} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -32,11 +34,17 @@ async def async_setup_entry( MBCover( coordinator, bee_id, - next(filter(lambda x: x.deviceID == 551, bee.actuators)).id, - next(filter(lambda x: x.deviceID == 552, bee.actuators)).id, + next( + (actuator.id for actuator in bee.actuators if actuator.deviceID == 551), + None, + ), + next( + (actuator.id for actuator in bee.actuators if actuator.deviceID == 552), + None, + ), ) for bee_id, bee in coordinator.data.bees.items() - if bee.productID == 47 + if bee.productID in COVER_IDS ) diff --git a/homeassistant/components/microbees/light.py b/homeassistant/components/microbees/light.py index 411eab22324..654cdc37182 100644 --- a/homeassistant/components/microbees/light.py +++ b/homeassistant/components/microbees/light.py @@ -60,19 +60,19 @@ class MBLight(MicroBeesActuatorEntity, LightEntity): sendCommand = await self.coordinator.microbees.sendCommand( self.actuator_id, 1, color=self._attr_rgbw_color ) - if sendCommand: - self.actuator.value = True - self.async_write_ha_state() - else: + if not sendCommand: raise HomeAssistantError(f"Failed to turn on {self.name}") + self.actuator.value = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" sendCommand = await self.coordinator.microbees.sendCommand( self.actuator_id, 0, color=self._attr_rgbw_color ) - if sendCommand: - self.actuator.value = False - self.async_write_ha_state() - else: + if not sendCommand: raise HomeAssistantError(f"Failed to turn off {self.name}") + + self.actuator.value = False + self.async_write_ha_state() diff --git a/homeassistant/components/microbees/strings.json b/homeassistant/components/microbees/strings.json index 6f17a12834e..49d42af83d3 100644 --- a/homeassistant/components/microbees/strings.json +++ b/homeassistant/components/microbees/strings.json @@ -19,7 +19,10 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "You can only reauthenticate this entry with the same microBees account." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/microbees/switch.py b/homeassistant/components/microbees/switch.py index 8e3c03e9ba4..1d668d041e1 100644 --- a/homeassistant/components/microbees/switch.py +++ b/homeassistant/components/microbees/switch.py @@ -56,17 +56,17 @@ class MBSwitch(MicroBeesActuatorEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" send_command = await self.coordinator.microbees.sendCommand(self.actuator_id, 1) - if send_command: - self.actuator.value = True - self.async_write_ha_state() - else: + if not send_command: raise HomeAssistantError(f"Failed to turn on {self.name}") + self.actuator.value = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" send_command = await self.coordinator.microbees.sendCommand(self.actuator_id, 0) - if send_command: - self.actuator.value = False - self.async_write_ha_state() - else: + if not send_command: raise HomeAssistantError(f"Failed to turn off {self.name}") + + self.actuator.value = False + self.async_write_ha_state() diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 6b94a621683..2830372f882 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -325,8 +325,6 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api: entry[CONF_PASSWORD], **kwargs, ) - _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) - return api except ( librouteros.exceptions.LibRouterosError, OSError, @@ -336,3 +334,6 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api: if "invalid user name or password" in str(api_error): raise LoginError from api_error raise CannotConnect from api_error + + _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) + return api diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 4ea63f5a472..f34067fea2e 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -23,13 +23,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, 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.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 5ec737c3f73..0a9eee6a0d5 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -121,10 +121,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> try: await api.async_initialize() - except MinecraftServerAddressError as error: + except MinecraftServerAddressError: _LOGGER.exception( - "Can't migrate configuration entry due to error while parsing server address, try again later: %s", - error, + "Can't migrate configuration entry due to error while parsing server address, try again later" ) return False diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 4b862f54715..fae004a015e 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, MutableMapping +from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -37,7 +37,7 @@ class MinecraftServerSensorEntityDescription(SensorEntityDescription): """Class describing Minecraft Server sensor entities.""" value_fn: Callable[[MinecraftServerData], StateType] - attributes_fn: Callable[[MinecraftServerData], MutableMapping[str, Any]] | None + attributes_fn: Callable[[MinecraftServerData], dict[str, Any]] | None supported_server_types: set[MinecraftServerType] diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index ab8c67f2ca9..dcb2eff2fd6 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -146,8 +146,7 @@ class MjpegCamera(Camera): async with asyncio.timeout(TIMEOUT): response = await websession.get(self._still_image_url, auth=self._auth) - image = await response.read() - return image + return await response.read() except TimeoutError: LOGGER.error("Timeout getting camera image from %s", self.name) diff --git a/homeassistant/components/moat/strings.json b/homeassistant/components/moat/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/moat/strings.json +++ b/homeassistant/components/moat/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 20a5448f2be..4c40e4f22b3 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -82,7 +82,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) hass.async_create_task( - discovery.async_load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config) + discovery.async_load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config), + eager_start=True, ) websocket_api.async_setup_commands(hass) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 1cac62ce964..f1f7b592621 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -17,6 +17,7 @@ from .const import ( ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_STATE, + ATTR_SENSOR_STATE_CLASS, SIGNAL_SENSOR_UPDATE, ) from .helpers import device_info @@ -44,6 +45,7 @@ class MobileAppEntity(RestoreEntity): """Update the entity from the config.""" config = self._config self._attr_device_class = config.get(ATTR_SENSOR_DEVICE_CLASS) + self._attr_state_class = config.get(ATTR_SENSOR_STATE_CLASS) self._attr_extra_state_attributes = config[ATTR_SENSOR_ATTRIBUTES] self._attr_icon = config[ATTR_SENSOR_ICON] self._attr_entity_category = config.get(ATTR_SENSOR_ENTITY_CATEGORY) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 13d50b7984f..0ecfe207277 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -38,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) def setup_decrypt( - key_encoder: type[RawEncoder] | type[HexEncoder], + key_encoder: type[RawEncoder | HexEncoder], ) -> Callable[[bytes, bytes], bytes]: """Return decryption function and length of key. @@ -55,7 +55,7 @@ def setup_decrypt( def setup_encrypt( - key_encoder: type[RawEncoder] | type[HexEncoder], + key_encoder: type[RawEncoder | HexEncoder], ) -> Callable[[bytes, bytes], bytes]: """Return encryption function and length of key. @@ -75,7 +75,7 @@ def _decrypt_payload_helper( key: str | bytes, ciphertext: bytes, key_bytes: bytes, - key_encoder: type[RawEncoder] | type[HexEncoder], + key_encoder: type[RawEncoder | HexEncoder], ) -> JsonValueType | None: """Decrypt encrypted payload.""" try: @@ -104,8 +104,7 @@ def _convert_legacy_encryption_key(key: str) -> bytes: keylen = SecretBox.KEY_SIZE key_bytes = key.encode("utf-8") key_bytes = key_bytes[:keylen] - key_bytes = key_bytes.ljust(keylen, b"\0") - return key_bytes + return key_bytes.ljust(keylen, b"\0") def decrypt_payload_legacy(key: str, ciphertext: bytes) -> JsonValueType | None: diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py index 6a863e6a75b..d9f7f4f04e1 100644 --- a/homeassistant/components/mobile_app/logbook.py +++ b/homeassistant/components/mobile_app/logbook.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, @@ -12,6 +13,7 @@ from homeassistant.components.logbook import ( ) from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.util.event_type import EventType from .const import DOMAIN @@ -21,7 +23,7 @@ IOS_EVENT_ZONE_EXITED = "ios.zone_exited" ATTR_ZONE = "zone" ATTR_SOURCE_DEVICE_NAME = "sourceDeviceName" ATTR_SOURCE_DEVICE_ID = "sourceDeviceID" -EVENT_TO_DESCRIPTION = { +EVENT_TO_DESCRIPTION: dict[EventType[Any] | str, str] = { IOS_EVENT_ZONE_ENTERED: "entered zone", IOS_EVENT_ZONE_EXITED: "exited zone", } diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 6f049d6f2d5..dd70cf1e22e 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -103,8 +103,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): self._async_update_attr_from_config() - @property - def native_value(self) -> StateType | date | datetime: + def _calculate_native_value(self) -> StateType | date | datetime: """Return the state of the sensor.""" if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): return None @@ -131,3 +130,4 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): config = self._config self._attr_native_unit_of_measurement = config.get(ATTR_SENSOR_UOM) self._attr_state_class = config.get(ATTR_SENSOR_STATE_CLASS) + self._attr_native_value = self._calculate_native_value() diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 83c3cb29cea..e862e4c8bd5 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -112,9 +112,6 @@ async def handle_push_notification_channel( if registered_channels.get(webhook_id) == channel: registered_channels.pop(webhook_id) - # Remove subscription from connection if still exists - connection.subscriptions.pop(msg["id"], None) - channel = registered_channels[webhook_id] = PushChannel( hass, webhook_id, diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 36ebb74edc3..c8714c902a3 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -45,8 +45,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: mochad_controller = MochadCtrl(host, port) - except exceptions.ConfigurationError as err: - _LOGGER.exception(str(err)) + except exceptions.ConfigurationError: + _LOGGER.exception("Unexpected exception") return False def stop_mochad(event): diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 23ad6ac1be6..08e927bb553 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -113,6 +113,13 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_VERIFY, @@ -134,6 +141,7 @@ from .modbus import ModbusHub, async_modbus_setup from .validators import ( check_hvac_target_temp_registers, duplicate_fan_mode_validator, + duplicate_swing_mode_validator, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -296,6 +304,21 @@ CLIMATE_SCHEMA = vol.All( duplicate_fan_mode_validator, ), ), + vol.Optional(CONF_SWING_MODE_REGISTER): vol.Maybe( + vol.All( + { + vol.Required(CONF_ADDRESS): register_int_list_validator, + CONF_SWING_MODE_VALUES: { + vol.Optional(CONF_SWING_MODE_SWING_ON): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_OFF): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_HORIZ): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_VERT): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_BOTH): cv.positive_int, + }, + }, + duplicate_swing_mode_validator, + ) + ), }, ), check_hvac_target_temp_registers, diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 5c8816dd74e..9f0e862f283 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -202,7 +202,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers.reverse() return registers - def __process_raw_value(self, entry: float | int | str | bytes) -> str | None: + def __process_raw_value(self, entry: float | str | bytes) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): return None diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 07dd12d3c94..0a4eae341b4 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime +import logging import struct from typing import Any, cast @@ -17,6 +18,11 @@ from homeassistant.components.climate import ( FAN_OFF, FAN_ON, FAN_TOP, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -28,6 +34,7 @@ from homeassistant.const import ( CONF_TEMPERATURE_UNIT, PRECISION_TENTHS, PRECISION_WHOLE, + STATE_UNKNOWN, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -67,6 +74,13 @@ from .const import ( CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_STEP, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -74,6 +88,8 @@ from .const import ( ) from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + PARALLEL_UPDATES = 1 HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY = { @@ -204,11 +220,35 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_fan_modes.append(fan_mode) else: - # No HVAC modes defined + # No FAN modes defined self._fan_mode_register = None self._attr_fan_mode = FAN_AUTO self._attr_fan_modes = [FAN_AUTO] + # No SWING modes defined + self._swing_mode_register = None + if CONF_SWING_MODE_REGISTER in config: + self._attr_supported_features = ( + self._attr_supported_features | ClimateEntityFeature.SWING_MODE + ) + mode_config = config[CONF_SWING_MODE_REGISTER] + self._swing_mode_register = mode_config[CONF_ADDRESS] + self._attr_swing_modes = cast(list[str], []) + self._attr_swing_mode = None + self._swing_mode_modbus_mapping: list[tuple[int, str]] = [] + mode_value_config = mode_config[CONF_SWING_MODE_VALUES] + for swing_mode_kw, swing_mode in ( + (CONF_SWING_MODE_SWING_ON, SWING_ON), + (CONF_SWING_MODE_SWING_OFF, SWING_OFF), + (CONF_SWING_MODE_SWING_HORIZ, SWING_HORIZONTAL), + (CONF_SWING_MODE_SWING_VERT, SWING_VERTICAL), + (CONF_SWING_MODE_SWING_BOTH, SWING_BOTH), + ): + if swing_mode_kw in mode_value_config: + value = mode_value_config[swing_mode_kw] + self._swing_mode_modbus_mapping.append((value, swing_mode)) + self._attr_swing_modes.append(swing_mode) + if CONF_HVAC_ONOFF_REGISTER in config: self._hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER] self._hvac_onoff_write_registers = config[CONF_WRITE_REGISTERS] @@ -287,6 +327,29 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): await self.async_update() + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing mode.""" + if self._swing_mode_register: + # Write a value to the mode register for the desired mode. + for value, smode in self._swing_mode_modbus_mapping: + if swing_mode == smode: + if isinstance(self._swing_mode_register, list): + await self._hub.async_pb_call( + self._slave, + self._swing_mode_register[0], + [value], + CALL_TYPE_WRITE_REGISTERS, + ) + else: + await self._hub.async_pb_call( + self._slave, + self._swing_mode_register, + value, + CALL_TYPE_WRITE_REGISTER, + ) + break + await self.async_update() + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temperature = ( @@ -387,6 +450,26 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): int(fan_mode), self._attr_fan_mode ) + # Read the Swing mode register if defined + if self._swing_mode_register: + swing_mode = await self._async_read_register( + CALL_TYPE_REGISTER_HOLDING, + self._swing_mode_register + if isinstance(self._swing_mode_register, int) + else self._swing_mode_register[0], + raw=True, + ) + + self._attr_swing_mode = STATE_UNKNOWN + for value, smode in self._swing_mode_modbus_mapping: + if swing_mode == value: + self._attr_swing_mode = smode + break + + if self._attr_swing_mode is STATE_UNKNOWN: + _err = f"{self.name}: No answer received from Swing mode register. State is Unknown" + _LOGGER.error(_err) + # Read the on/off register if defined. If the value in this # register is "OFF", it will take precedence over the value # in the mode register. diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 425bd744a1e..02f5d99c72c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -70,6 +70,13 @@ CONF_HVAC_MODE_AUTO = "state_auto" CONF_HVAC_MODE_DRY = "state_dry" CONF_HVAC_MODE_FAN_ONLY = "state_fan_only" CONF_HVAC_MODE_VALUES = "values" +CONF_SWING_MODE_REGISTER = "swing_mode_register" +CONF_SWING_MODE_SWING_BOTH = "swing_mode_state_both" +CONF_SWING_MODE_SWING_HORIZ = "swing_mode_state_horizontal" +CONF_SWING_MODE_SWING_OFF = "swing_mode_state_off" +CONF_SWING_MODE_SWING_ON = "swing_mode_state_on" +CONF_SWING_MODE_SWING_VERT = "swing_mode_state_vertical" +CONF_SWING_MODE_VALUES = "values" CONF_WRITE_REGISTERS = "write_registers" CONF_VERIFY = "verify" CONF_VIRTUAL_COUNT = "virtual_count" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0d1848e0d8e..a5c0867dedb 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -34,6 +34,7 @@ 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 @@ -234,6 +235,18 @@ async def async_modbus_setup( async def async_restart_hub(service: ServiceCall) -> None: """Restart Modbus hub.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_restart", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_restart", + ) + _LOGGER.warning( + "`modbus.restart` is deprecated and will be removed in version 2024.11" + ) async_dispatcher_send(hass, SIGNAL_START_ENTITY) hub = hub_collect[service.data[ATTR_HUB]] await hub.async_restart() diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index fd93185b891..f89f9a97d52 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -88,7 +88,7 @@ }, "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." + "description": "An address can only be associated with one 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.", @@ -97,6 +97,10 @@ "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." + }, + "deprecated_restart": { + "title": "`modbus.restart` is being removed", + "description": "Please use reload yaml via the developer tools in the UI instead of via the `modbus.restart` service." } } } diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 7de2ecbe604..5071d098db7 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -42,6 +42,8 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, @@ -256,8 +258,25 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config +def duplicate_swing_mode_validator(config: dict[str, Any]) -> dict: + """Control modbus climate swing mode values for duplicates.""" + swing_modes: set[int] = set() + errors = [] + for key, value in config[CONF_SWING_MODE_VALUES].items(): + if value in swing_modes: + warn = f"Modbus swing mode {key} has a duplicate value {value}, not loaded, values must be unique!" + _LOGGER.warning(warn) + errors.append(key) + else: + swing_modes.add(value) + + for key in reversed(errors): + del config[CONF_SWING_MODE_VALUES][key] + return config + + def check_hvac_target_temp_registers(config: dict) -> dict: - """Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes.""" + """Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes, Swing Modes.""" if ( CONF_HVAC_MODE_REGISTER in config @@ -281,6 +300,17 @@ def check_hvac_target_temp_registers(config: dict) -> dict: _LOGGER.warning(wrn) del config[CONF_FAN_MODE_REGISTER] + if CONF_SWING_MODE_REGISTER in config: + regToTest = ( + config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS] + if isinstance(config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS], int) + else config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS][0] + ) + if regToTest in config[CONF_TARGET_TEMP]: + wrn = f"{CONF_SWING_MODE_REGISTER} overlaps CONF_TARGET_TEMP register(s). {CONF_SWING_MODE_REGISTER} is not loaded!" + _LOGGER.warning(wrn) + del config[CONF_SWING_MODE_REGISTER] + return config @@ -294,7 +324,7 @@ def register_int_list_validator(value: Any) -> Any: return value raise vol.Invalid( - f"Invalid {CONF_ADDRESS} register for fan mode. Required type: positive integer, allowed 1 or list of 1 register." + f"Invalid {CONF_ADDRESS} register for fan/swing mode. Required type: positive integer, allowed 1 or list of 1 register." ) @@ -421,6 +451,12 @@ def validate_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}") + if CONF_SWING_MODE_REGISTER in entity: + loc_addr.add( + f"{hub_name}{entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS] + if isinstance(entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS],int) + else entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS][0]}_{inx}" + ) dup_addrs = ent_addr.intersection(loc_addr) if len(dup_addrs) > 0: diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 6839e57c838..cbb531d9672 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -17,13 +17,16 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + 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.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import METRIC_SYSTEM diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 57282fb6545..c7683ebedd6 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -57,11 +57,25 @@ 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) - if unload_ok: - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - hass.data[DOMAIN].pop(entry.entry_id) + if not unload_ok: + return False - return unload_ok + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + def _cleanup(monoprice) -> None: + """Destroy the Monoprice object. + + Destroying the Monoprice closes the serial connection, do it in an executor so the garbage + collection does not block. + """ + del monoprice + + monoprice = hass.data[DOMAIN][entry.entry_id][MONOPRICE_OBJECT] + hass.data[DOMAIN].pop(entry.entry_id) + + await hass.async_add_executor_job(_cleanup, monoprice) + + return True async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py index 1c424c866e4..d8aa082ee3a 100644 --- a/homeassistant/components/moon/config_flow.py +++ b/homeassistant/components/moon/config_flow.py @@ -18,9 +18,6 @@ class MoonConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> 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={}) diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 6102b37fb13..519df85fc9c 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/moon", "integration_type": "service", "iot_class": "calculated", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json index 22b430731e0..e0e2c9ea6f4 100644 --- a/homeassistant/components/moon/strings.json +++ b/homeassistant/components/moon/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": { diff --git a/homeassistant/components/mopeka/strings.json b/homeassistant/components/mopeka/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/mopeka/strings.json +++ b/homeassistant/components/mopeka/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/motionmount/manifest.json b/homeassistant/components/motionmount/manifest.json index bfe7e21fce9..e6a7bd50fba 100644 --- a/homeassistant/components/motionmount/manifest.json +++ b/homeassistant/components/motionmount/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/motionmount", "integration_type": "device", "iot_class": "local_push", - "requirements": ["python-MotionMount==0.3.1"], + "requirements": ["python-MotionMount==1.0.0"], "zeroconf": ["_tvm._tcp.local."] } diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8e866776a41..cc1ae3ddce1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Callable from datetime import datetime import logging -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, cast import voluptuous as vol @@ -143,8 +143,6 @@ CONFIG_ENTRY_CONFIG_KEYS = [ CONF_WILL_MESSAGE, ] -_T = TypeVar("_T") - REMOVED_OPTIONS = vol.All( cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4 cv.removed(CONF_BROKER), # Removed in HA Core 2023.4 @@ -267,7 +265,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf: dict[str, Any] mqtt_data: MqttData - async def _setup_client() -> tuple[MqttData, dict[str, Any]]: + async def _setup_client( + client_available: asyncio.Future[bool], + ) -> tuple[MqttData, dict[str, Any]]: """Set up the MQTT client.""" # Fetch configuration conf = dict(entry.data) @@ -296,7 +296,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.add_update_listener(_async_config_entry_updated) ) - await mqtt_data.client.async_connect() + await mqtt_data.client.async_connect(client_available) return (mqtt_data, conf) client_available: asyncio.Future[bool] @@ -305,13 +305,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: client_available = hass.data[DATA_MQTT_AVAILABLE] - setup_ok: bool = False - try: - mqtt_data, conf = await _setup_client() - setup_ok = True - finally: - if not client_available.done(): - client_available.set_result(setup_ok) + mqtt_data, conf = await _setup_client(client_available) async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index b2fab355c41..d79492ccb27 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -3,12 +3,14 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable +import contextlib from dataclasses import dataclass -from functools import lru_cache +from functools import lru_cache, partial from itertools import chain, groupby import logging from operator import attrgetter +import socket import ssl import time from typing import TYPE_CHECKING, Any @@ -23,22 +25,16 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import ( - CALLBACK_TYPE, - CoreState, - Event, - HassJob, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import create_eager_task from homeassistant.util.logging import catch_log_exception from .const import ( @@ -92,6 +88,9 @@ INITIAL_SUBSCRIBE_COOLDOWN = 1.0 SUBSCRIBE_COOLDOWN = 0.1 UNSUBSCRIBE_COOLDOWN = 0.1 TIMEOUT_ACK = 10 +RECONNECT_INTERVAL_SECONDS = 10 + +SocketType = socket.socket | ssl.SSLSocket | Any SubscribePayloadType = str | bytes # Only bytes if encoding is None @@ -187,7 +186,7 @@ async def async_subscribe( translation_domain=DOMAIN, translation_placeholders={"topic": topic}, ) from exc - async_remove = await mqtt_data.client.async_subscribe( + return await mqtt_data.client.async_subscribe( topic, catch_log_exception( msg_callback, @@ -199,7 +198,6 @@ async def async_subscribe( qos, encoding, ) - return async_remove @bind_hass @@ -259,7 +257,9 @@ class MqttClientSetup: # However, that feature is not mandatory so we generate our own. client_id = mqtt.base62(uuid.uuid4().int, padding=22) transport = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT) - self._client = mqtt.Client(client_id, protocol=proto, transport=transport) + self._client = mqtt.Client( + client_id, protocol=proto, transport=transport, reconnect_on_failure=False + ) # Enable logging self._client.enable_logger() @@ -346,7 +346,7 @@ class EnsureJobAfterCooldown: return self._async_cancel_timer() - self._task = asyncio.create_task(self._async_job()) + self._task = create_eager_task(self._async_job()) self._task.add_done_callback(self._async_task_done) @callback @@ -405,37 +405,39 @@ class MQTT: self._ha_started = asyncio.Event() self._cleanup_on_unload: list[Callable[[], None]] = [] - self._paho_lock = asyncio.Lock() # Prevents parallel calls to the MQTT client + self._connection_lock = asyncio.Lock() self._pending_operations: dict[int, asyncio.Event] = {} self._pending_operations_condition = asyncio.Condition() self._subscribe_debouncer = EnsureJobAfterCooldown( INITIAL_SUBSCRIBE_COOLDOWN, self._async_perform_subscriptions ) + self._misc_task: asyncio.Task | None = None + self._reconnect_task: asyncio.Task | None = None + self._should_reconnect: bool = True + self._available_future: asyncio.Future[bool] | None = None + self._max_qos: dict[str, int] = {} # topic, max qos self._pending_subscriptions: dict[str, int] = {} # topic, qos self._unsubscribe_debouncer = EnsureJobAfterCooldown( UNSUBSCRIBE_COOLDOWN, self._async_perform_unsubscribes ) self._pending_unsubscribes: set[str] = set() # topic - - if self.hass.state is CoreState.running: - self._ha_started.set() - else: - - @callback - def ha_started(_: Event) -> None: - self._ha_started.set() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) - - async def async_stop_mqtt(_event: Event) -> None: - """Stop MQTT component.""" - await self.async_disconnect() - - self._cleanup_on_unload.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + self._cleanup_on_unload.extend( + ( + async_at_started(hass, self._async_ha_started), + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self._async_ha_stop), + ) ) + @callback + def _async_ha_started(self, _hass: HomeAssistant) -> None: + """Handle HA started.""" + self._ha_started.set() + + async def _async_ha_stop(self, _event: Event) -> None: + """Handle HA stop.""" + await self.async_disconnect() + def start( self, mqtt_data: MqttData, @@ -457,25 +459,140 @@ class MQTT: while self._cleanup_on_unload: self._cleanup_on_unload.pop()() + @contextlib.asynccontextmanager + async def _async_connect_in_executor(self) -> AsyncGenerator[None, None]: + # While we are connecting in the executor we need to + # handle on_socket_open and on_socket_register_write + # in the executor as well. + mqttc = self._mqttc + try: + mqttc.on_socket_open = self._on_socket_open + mqttc.on_socket_register_write = self._on_socket_register_write + yield + finally: + # Once the executor job is done, we can switch back to + # handling these in the event loop. + mqttc.on_socket_open = self._async_on_socket_open + mqttc.on_socket_register_write = self._async_on_socket_register_write + def init_client(self) -> None: """Initialize paho client.""" - self._mqttc = MqttClientSetup(self.conf).client - self._mqttc.on_connect = self._mqtt_on_connect - self._mqttc.on_disconnect = self._mqtt_on_disconnect - self._mqttc.on_message = self._mqtt_on_message - self._mqttc.on_publish = self._mqtt_on_callback - self._mqttc.on_subscribe = self._mqtt_on_callback - self._mqttc.on_unsubscribe = self._mqtt_on_callback + mqttc = MqttClientSetup(self.conf).client + # on_socket_unregister_write and _async_on_socket_close + # are only ever called in the event loop + mqttc.on_socket_close = self._async_on_socket_close + mqttc.on_socket_unregister_write = self._async_on_socket_unregister_write + + # These will be called in the event loop + mqttc.on_connect = self._async_mqtt_on_connect + mqttc.on_disconnect = self._async_mqtt_on_disconnect + mqttc.on_message = self._async_mqtt_on_message + mqttc.on_publish = self._async_mqtt_on_callback + mqttc.on_subscribe = self._async_mqtt_on_callback + mqttc.on_unsubscribe = self._async_mqtt_on_callback if will := self.conf.get(CONF_WILL_MESSAGE, DEFAULT_WILL): will_message = PublishMessage(**will) - self._mqttc.will_set( + mqttc.will_set( topic=will_message.topic, payload=will_message.payload, qos=will_message.qos, retain=will_message.retain, ) + self._mqttc = mqttc + + async def _misc_loop(self) -> None: + """Start the MQTT client misc loop.""" + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + + while self._mqttc.loop_misc() == mqtt.MQTT_ERR_SUCCESS: + await asyncio.sleep(1) + + @callback + def _async_reader_callback(self, client: mqtt.Client) -> None: + """Handle reading data from the socket.""" + if (status := client.loop_read()) != 0: + self._async_on_disconnect(status) + + @callback + def _async_start_misc_loop(self) -> None: + """Start the misc loop.""" + if self._misc_task is None or self._misc_task.done(): + _LOGGER.debug("%s: Starting client misc loop", self.config_entry.title) + self._misc_task = self.config_entry.async_create_background_task( + self.hass, self._misc_loop(), name="mqtt misc loop" + ) + + def _on_socket_open( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Handle socket open.""" + self.loop.call_soon_threadsafe( + self._async_on_socket_open, client, userdata, sock + ) + + @callback + def _async_on_socket_open( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Handle socket open.""" + fileno = sock.fileno() + _LOGGER.debug("%s: connection opened %s", self.config_entry.title, fileno) + if fileno > -1: + self.loop.add_reader(sock, partial(self._async_reader_callback, client)) + self._async_start_misc_loop() + + @callback + def _async_on_socket_close( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Handle socket close.""" + fileno = sock.fileno() + _LOGGER.debug("%s: connection closed %s", self.config_entry.title, fileno) + # If socket close is called before the connect + # result is set make sure the first connection result is set + self._async_connection_result(False) + if fileno > -1: + self.loop.remove_reader(sock) + if self._misc_task is not None and not self._misc_task.done(): + self._misc_task.cancel() + + @callback + def _async_writer_callback(self, client: mqtt.Client) -> None: + """Handle writing data to the socket.""" + if (status := client.loop_write()) != 0: + self._async_on_disconnect(status) + + def _on_socket_register_write( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Register the socket for writing.""" + self.loop.call_soon_threadsafe( + self._async_on_socket_register_write, client, None, sock + ) + + @callback + def _async_on_socket_register_write( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Register the socket for writing.""" + fileno = sock.fileno() + _LOGGER.debug("%s: register write %s", self.config_entry.title, fileno) + if fileno > -1: + self.loop.add_writer(sock, partial(self._async_writer_callback, client)) + + @callback + def _async_on_socket_unregister_write( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Unregister the socket for writing.""" + fileno = sock.fileno() + _LOGGER.debug("%s: unregister write %s", self.config_entry.title, fileno) + if fileno > -1: + self.loop.remove_writer(sock) + def _is_active_subscription(self, topic: str) -> bool: """Check if a topic has an active subscription.""" return topic in self._simple_subscriptions or any( @@ -486,10 +603,7 @@ class MQTT: self, topic: str, payload: PublishPayloadType, qos: int, retain: bool ) -> None: """Publish a MQTT message.""" - async with self._paho_lock: - msg_info = await self.hass.async_add_executor_job( - self._mqttc.publish, topic, payload, qos, retain - ) + msg_info = self._mqttc.publish(topic, payload, qos, retain) _LOGGER.debug( "Transmitting%s message on %s: '%s', mid: %s, qos: %s", " retained" if retain else "", @@ -501,37 +615,71 @@ class MQTT: _raise_on_error(msg_info.rc) await self._wait_for_mid(msg_info.mid) - async def async_connect(self) -> None: + async def async_connect(self, client_available: asyncio.Future[bool]) -> None: """Connect to the host. Does not process messages yet.""" # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt result: int | None = None + self._available_future = client_available + self._should_reconnect = True try: - result = await self.hass.async_add_executor_job( - self._mqttc.connect, - self.conf[CONF_BROKER], - self.conf.get(CONF_PORT, DEFAULT_PORT), - self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE), - ) + async with self._connection_lock, self._async_connect_in_executor(): + result = await self.hass.async_add_executor_job( + self._mqttc.connect, + self.conf[CONF_BROKER], + self.conf.get(CONF_PORT, DEFAULT_PORT), + self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE), + ) except OSError as err: _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) + self._async_connection_result(False) + finally: + if result is not None and result != 0: + if result is not None: + _LOGGER.error( + "Failed to connect to MQTT server: %s", + mqtt.error_string(result), + ) + self._async_connection_result(False) - if result is not None and result != 0: - _LOGGER.error( - "Failed to connect to MQTT server: %s", mqtt.error_string(result) + @callback + def _async_connection_result(self, connected: bool) -> None: + """Handle a connection result.""" + if self._available_future and not self._available_future.done(): + self._available_future.set_result(connected) + + if connected: + self._async_cancel_reconnect() + elif self._should_reconnect and not self._reconnect_task: + self._reconnect_task = self.config_entry.async_create_background_task( + self.hass, self._reconnect_loop(), "mqtt reconnect loop" ) - self._mqttc.loop_start() + @callback + def _async_cancel_reconnect(self) -> None: + """Cancel the reconnect task.""" + if self._reconnect_task: + self._reconnect_task.cancel() + self._reconnect_task = None + + async def _reconnect_loop(self) -> None: + """Reconnect to the MQTT server.""" + while True: + if not self.connected: + try: + async with self._connection_lock, self._async_connect_in_executor(): + await self.hass.async_add_executor_job(self._mqttc.reconnect) + except OSError as err: + _LOGGER.debug( + "Error re-connecting to MQTT server due to exception: %s", err + ) + + await asyncio.sleep(RECONNECT_INTERVAL_SECONDS) async def async_disconnect(self) -> None: """Stop the MQTT client.""" - def stop() -> None: - """Stop the MQTT client.""" - # Do not disconnect, we want the broker to always publish will - self._mqttc.loop_stop() - def no_more_acks() -> bool: """Return False if there are unprocessed ACKs.""" return not any(not op.is_set() for op in self._pending_operations.values()) @@ -550,8 +698,11 @@ class MQTT: await self._pending_operations_condition.wait_for(no_more_acks) # stop the MQTT loop - async with self._paho_lock: - await self.hass.async_add_executor_job(stop) + async with self._connection_lock: + self._should_reconnect = False + self._async_cancel_reconnect() + # We do not gracefully disconnect to ensure + # the broker publishes the will message @callback def async_restore_tracked_subscriptions( @@ -690,11 +841,8 @@ class MQTT: subscriptions: dict[str, int] = self._pending_subscriptions self._pending_subscriptions = {} - async with self._paho_lock: - subscription_list = list(subscriptions.items()) - result, mid = await self.hass.async_add_executor_job( - self._mqttc.subscribe, subscription_list - ) + subscription_list = list(subscriptions.items()) + result, mid = self._mqttc.subscribe(subscription_list) for topic, qos in subscriptions.items(): _LOGGER.debug("Subscribing to %s, mid: %s, qos: %s", topic, mid, qos) @@ -713,17 +861,31 @@ class MQTT: topics = list(self._pending_unsubscribes) self._pending_unsubscribes = set() - async with self._paho_lock: - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topics - ) + result, mid = self._mqttc.unsubscribe(topics) _raise_on_error(result) for topic in topics: _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) await self._wait_for_mid(mid) - def _mqtt_on_connect( + async def _async_resubscribe_and_publish_birth_message( + self, birth_message: PublishMessage + ) -> None: + """Resubscribe to all topics and publish birth message.""" + await self._async_perform_subscriptions() + await self._ha_started.wait() # Wait for Home Assistant to start + await self._discovery_cooldown() # Wait for MQTT discovery to cool down + # Update subscribe cooldown period to a shorter time + self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) + await self.async_publish( + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, + ) + + @callback + def _async_mqtt_on_connect( self, _mqttc: mqtt.Client, _userdata: None, @@ -740,14 +902,22 @@ class MQTT: import paho.mqtt.client as mqtt if result_code != mqtt.CONNACK_ACCEPTED: + if result_code in ( + mqtt.CONNACK_REFUSED_BAD_USERNAME_PASSWORD, + mqtt.CONNACK_REFUSED_NOT_AUTHORIZED, + ): + self._should_reconnect = False + self.hass.async_create_task(self.async_disconnect()) + self.config_entry.async_start_reauth(self.hass) _LOGGER.error( "Unable to connect to the MQTT broker: %s", mqtt.connack_string(result_code), ) + self._async_connection_result(False) return self.connected = True - dispatcher_send(self.hass, MQTT_CONNECTED) + async_dispatcher_send(self.hass, MQTT_CONNECTED) _LOGGER.info( "Connected to MQTT server %s:%s (%s)", self.conf[CONF_BROKER], @@ -755,32 +925,33 @@ class MQTT: result_code, ) - self.hass.create_task(self._async_resubscribe()) - + self._async_queue_resubscribe() + birth: dict[str, Any] if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH): - - async def publish_birth_message(birth_message: PublishMessage) -> None: - await self._ha_started.wait() # Wait for Home Assistant to start - await self._discovery_cooldown() # Wait for MQTT discovery to cool down - # Update subscribe cooldown period to a shorter time - self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) - await self.async_publish( - topic=birth_message.topic, - payload=birth_message.payload, - qos=birth_message.qos, - retain=birth_message.retain, - ) - birth_message = PublishMessage(**birth) - asyncio.run_coroutine_threadsafe( - publish_birth_message(birth_message), self.hass.loop + self.config_entry.async_create_background_task( + self.hass, + self._async_resubscribe_and_publish_birth_message(birth_message), + name="mqtt re-subscribe and birth", ) else: # Update subscribe cooldown period to a shorter time + self.config_entry.async_create_background_task( + self.hass, + self._async_perform_subscriptions(), + name="mqtt re-subscribe", + ) self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) - async def _async_resubscribe(self) -> None: - """Resubscribe on reconnect.""" + self._async_connection_result(True) + + @callback + def _async_queue_resubscribe(self) -> None: + """Queue subscriptions on reconnect. + + self._async_perform_subscriptions must be called + after this method to actually subscribe. + """ self._max_qos.clear() self._retained_topics.clear() # Group subscriptions to only re-subscribe once for each topic. @@ -795,17 +966,6 @@ class MQTT: ], queue_only=True, ) - await self._async_perform_subscriptions() - - def _mqtt_on_message( - self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage - ) -> None: - """Message received callback.""" - # MQTT messages tend to be high volume, - # and since they come in via a thread and need to be processed in the event loop, - # we want to avoid hass.add_job since most of the time is spent calling - # inspect to figure out how to run the callback. - self.loop.call_soon_threadsafe(self._mqtt_handle_message, msg) @lru_cache(None) # pylint: disable=method-cache-max-size-none def _matching_subscriptions(self, topic: str) -> list[Subscription]: @@ -820,7 +980,9 @@ class MQTT: return subscriptions @callback - def _mqtt_handle_message(self, msg: mqtt.MQTTMessage) -> None: + def _async_mqtt_on_message( + self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage + ) -> None: topic = msg.topic # msg.topic is a property that decodes the topic to a string # every time it is accessed. Save the result to avoid @@ -835,6 +997,7 @@ class MQTT: timestamp = dt_util.utcnow() subscriptions = self._matching_subscriptions(topic) + msg_cache_by_subscription_topic: dict[str, ReceiveMessage] = {} for subscription in subscriptions: if msg.retain: @@ -858,20 +1021,28 @@ class MQTT: subscription.job, ) continue - self.hass.async_run_hass_job( - subscription.job, - ReceiveMessage( + subscription_topic = subscription.topic + if subscription_topic not in msg_cache_by_subscription_topic: + # Only make one copy of the message + # per topic so we avoid storing a separate + # dataclass in memory for each subscriber + # to the same topic for retained messages + receive_msg = ReceiveMessage( topic, payload, msg.qos, msg.retain, - subscription.topic, + subscription_topic, timestamp, - ), - ) + ) + msg_cache_by_subscription_topic[subscription_topic] = receive_msg + else: + receive_msg = msg_cache_by_subscription_topic[subscription_topic] + self.hass.async_run_hass_job(subscription.job, receive_msg) self._mqtt_data.state_write_requests.process_write_state_requests(msg) - def _mqtt_on_callback( + @callback + def _async_mqtt_on_callback( self, _mqttc: mqtt.Client, _userdata: None, @@ -883,7 +1054,9 @@ class MQTT: # The callback signature for on_unsubscribe is different from on_subscribe # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reasoncodes are not used in Home Assistant - self.hass.create_task(self._mqtt_handle_mid(mid)) + self.config_entry.async_create_task( + self.hass, self._mqtt_handle_mid(mid), name=f"mqtt handle mid {mid}" + ) async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid @@ -899,7 +1072,8 @@ class MQTT: if mid not in self._pending_operations: self._pending_operations[mid] = asyncio.Event() - def _mqtt_on_disconnect( + @callback + def _async_mqtt_on_disconnect( self, _mqttc: mqtt.Client, _userdata: None, @@ -907,8 +1081,19 @@ class MQTT: properties: mqtt.Properties | None = None, ) -> None: """Disconnected callback.""" + self._async_on_disconnect(result_code) + + @callback + def _async_on_disconnect(self, result_code: int) -> None: + if not self.connected: + # This function is re-entrant and may be called multiple times + # when there is a broken pipe error. + return + # If disconnect is called before the connect + # result is set make sure the first connection result is set + self._async_connection_result(False) self.connected = False - dispatcher_send(self.hass, MQTT_DISCONNECTED) + async_dispatcher_send(self.hass, MQTT_DISCONNECTED) _LOGGER.warning( "Disconnected from MQTT server %s:%s (%s)", self.conf[CONF_BROKER], diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5bf0c9c1879..1a7dfbbc507 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import OrderedDict -from collections.abc import Callable +from collections.abc import Callable, Mapping import queue from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError from types import MappingProxyType @@ -158,13 +158,46 @@ CERT_UPLOAD_SELECTOR = FileSelector( ) KEY_UPLOAD_SELECTOR = FileSelector(FileSelectorConfig(accept=".key,application/pkcs8")) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): TEXT_SELECTOR, + vol.Required(CONF_PASSWORD): PASSWORD_SELECTOR, + } +) +PWD_NOT_CHANGED = "__**password_not_changed**__" + + +@callback +def update_password_from_user_input( + entry_password: str | None, user_input: dict[str, Any] +) -> dict[str, Any]: + """Update the password if the entry has been updated. + + As we want to avoid reflecting the stored password in the UI, + we replace the suggested value in the UI with a sentitel, + and we change it back here if it was changed. + """ + substituted_used_data = dict(user_input) + # Take out the password submitted + user_password: str | None = substituted_used_data.pop(CONF_PASSWORD, None) + # Only add the password if it has changed. + # If the sentinel password is submitted, we replace that with our current + # password from the config entry data. + password_changed = user_password is not None and user_password != PWD_NOT_CHANGED + password = user_password if password_changed else entry_password + if password is not None: + substituted_used_data[CONF_PASSWORD] = password + return substituted_used_data + class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 + entry: ConfigEntry | None _hassio_discovery: dict[str, Any] | None = None + _reauth_config_entry: ConfigEntry | None = None @staticmethod @callback @@ -183,6 +216,49 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_broker() + 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"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm re-authentication with MQTT broker.""" + errors: dict[str, str] = {} + + assert self.entry is not None + if user_input: + substituted_used_data = update_password_from_user_input( + self.entry.data.get(CONF_PASSWORD), user_input + ) + new_entry_data = {**self.entry.data, **substituted_used_data} + if await self.hass.async_add_executor_job( + try_connection, + new_entry_data, + ): + return self.async_update_reload_and_abort( + self.entry, data=new_entry_data + ) + + errors["base"] = "invalid_auth" + + schema = self.add_suggested_values_to_schema( + REAUTH_SCHEMA, + { + CONF_USERNAME: self.entry.data.get(CONF_USERNAME), + CONF_PASSWORD: PWD_NOT_CHANGED, + }, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=schema, + errors=errors, + ) + async def async_step_broker( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -291,13 +367,17 @@ class MQTTOptionsFlowHandler(OptionsFlow): validated_user_input, errors, ): + self.broker_config.update( + update_password_from_user_input( + self.config_entry.data.get(CONF_PASSWORD), validated_user_input + ), + ) can_connect = await self.hass.async_add_executor_job( try_connection, - validated_user_input, + self.broker_config, ) if can_connect: - self.broker_config.update(validated_user_input) return await self.async_step_options() errors["base"] = "cannot_connect" @@ -598,7 +678,9 @@ async def async_get_broker_settings( current_broker = current_config.get(CONF_BROKER) current_port = current_config.get(CONF_PORT, DEFAULT_PORT) current_user = current_config.get(CONF_USERNAME) - current_pass = current_config.get(CONF_PASSWORD) + # Return the sentinel password to avoid exposure + current_entry_pass = current_config.get(CONF_PASSWORD) + current_pass = PWD_NOT_CHANGED if current_entry_pass else None # Treat the previous post as an update of the current settings # (if there was a basic broker setup step) diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 2500923ca9b..7244a41e975 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -45,6 +45,7 @@ CONFIG_SCHEMA_BASE = vol.Schema( Platform.LAWN_MOWER.value: vol.All(cv.ensure_list, [dict]), Platform.LIGHT.value: vol.All(cv.ensure_list, [dict]), Platform.LOCK.value: vol.All(cv.ensure_list, [dict]), + Platform.NOTIFY.value: vol.All(cv.ensure_list, [dict]), Platform.NUMBER.value: vol.All(cv.ensure_list, [dict]), Platform.SCENE.value: vol.All(cv.ensure_list, [dict]), Platform.SELECT.value: vol.All(cv.ensure_list, [dict]), diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 82320cd2f11..7eca266edfa 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -157,6 +157,7 @@ RELOADABLE_PLATFORMS = [ Platform.LIGHT, Platform.LAWN_MOWER, Platform.LOCK, + Platform.NOTIFY, Platform.NUMBER, Platform.SCENE, Platform.SELECT, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 13c56a9b48e..e330cd9b44b 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -66,6 +66,7 @@ SUPPORTED_COMPONENTS = { "lawn_mower", "light", "lock", + "notify", "number", "scene", "siren", @@ -268,7 +269,7 @@ async def async_start( # noqa: C901 availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" # If present, the node_id will be included in the discovered object id - discovery_id = " ".join((node_id, object_id)) if node_id else object_id + discovery_id = f"{node_id} {object_id}" if node_id else object_id discovery_hash = (component, discovery_id) if discovery_payload: diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 052fa394248..bf0de319df0 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -712,7 +712,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): keys.append("white") elif color_mode == ColorMode.RGBWW: keys.extend(["cold_white", "warm_white"]) - variables = dict(zip(keys, color)) + variables = dict(zip(keys, color, strict=False)) return self._command_templates[template](rgb_color_str, variables) def set_optimistic( diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 3a284c6719c..34370c82507 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -1,11 +1,11 @@ { "domain": "mqtt", "name": "MQTT", - "codeowners": ["@emontnemery", "@jbouwh"], + "codeowners": ["@emontnemery", "@jbouwh", "@bdraco"], "config_flow": true, "dependencies": ["file_upload", "http"], "documentation": "https://www.home-assistant.io/integrations/mqtt", "iot_class": "local_push", - "quality_scale": "gold", + "quality_scale": "platinum", "requirements": ["paho-mqtt==1.6.1"] } diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 42ad807d2f1..63df7c71c09 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -278,8 +278,8 @@ def async_handle_schema_error( async def _async_discover( hass: HomeAssistant, domain: str, - setup: partial[CALLBACK_TYPE] | None, - async_setup: partial[Coroutine[Any, Any, None]] | None, + setup: Callable[[MQTTDiscoveryPayload], None] | None, + async_setup: Callable[[MQTTDiscoveryPayload], Coroutine[Any, Any, None]] | None, discovery_payload: MQTTDiscoveryPayload, ) -> None: """Discover and add an MQTT entity, automation or tag. @@ -314,10 +314,18 @@ async def _async_discover( raise +class _SetupNonEntityHelperCallbackProtocol(Protocol): # pragma: no cover + """Callback protocol for async_setup in async_setup_non_entity_entry_helper.""" + + async def __call__( + self, config: ConfigType, discovery_data: DiscoveryInfoType + ) -> None: ... + + async def async_setup_non_entity_entry_helper( hass: HomeAssistant, domain: str, - async_setup: partial[Coroutine[Any, Any, None]], + async_setup: _SetupNonEntityHelperCallbackProtocol, discovery_schema: vol.Schema, ) -> None: """Set up automation or tag creation dynamically through MQTT discovery.""" @@ -327,7 +335,7 @@ async def async_setup_non_entity_entry_helper( discovery_payload: MQTTDiscoveryPayload, ) -> None: """Set up an MQTT entity, automation or tag from discovery.""" - config: DiscoveryInfoType = discovery_schema(discovery_payload) + config: ConfigType = discovery_schema(discovery_payload) await async_setup(config, discovery_data=discovery_payload.discovery_data) mqtt_data.reload_dispatchers.append( @@ -1007,7 +1015,8 @@ class MqttDiscoveryUpdate(Entity): self.hass.async_create_task( _async_process_discovery_update_and_remove( payload, self._discovery_data - ) + ), + eager_start=False, ) elif self._discovery_update: if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: @@ -1016,7 +1025,8 @@ class MqttDiscoveryUpdate(Entity): self.hass.async_create_task( _async_process_discovery_update( payload, self._discovery_update, self._discovery_data - ) + ), + eager_start=False, ) else: # Non-empty, unchanged payload: Ignore to avoid changing states diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 6e6ae784eec..f53643268e7 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -58,7 +58,7 @@ class PublishMessage: retain: bool -@dataclass +@dataclass(slots=True, frozen=True) class ReceiveMessage: """MQTT Message received.""" diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py new file mode 100644 index 00000000000..b7a17f07f7f --- /dev/null +++ b/homeassistant/components/mqtt/notify.py @@ -0,0 +1,95 @@ +"""Support for MQTT notify.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components import notify +from homeassistant.components.notify import NotifyEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType + +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA +from .const import ( + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, +) +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entity_entry_helper, +) +from .models import MqttCommandTemplate +from .util import valid_publish_topic + +DEFAULT_NAME = "MQTT notify" + +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( + { + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT notify through YAML and through MQTT discovery.""" + await async_setup_entity_entry_helper( + hass, + config_entry, + MqttNotify, + notify.DOMAIN, + async_add_entities, + DISCOVERY_SCHEMA, + PLATFORM_SCHEMA_MODERN, + ) + + +class MqttNotify(MqttEntity, NotifyEntity): + """Representation of a notification entity service that can send messages using MQTT.""" + + _default_name = DEFAULT_NAME + _entity_id_format = notify.ENTITY_ID_FORMAT + + @staticmethod + def config_schema() -> vol.Schema: + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), entity=self + ).async_render + + def _prepare_subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + + async def async_send_message(self, message: str) -> None: + """Send a message.""" + payload = self._command_template(message) + await self.async_publish( + self._config[CONF_COMMAND_TOPIC], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 87fe0bd033a..fc5f0bc4970 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -68,10 +68,23 @@ "data_description": { "discovery": "Option to enable MQTT automatic discovery." } + }, + "reauth_confirm": { + "title": "Re-authentication required with the MQTT broker", + "description": "The MQTT broker reported an authentication error. Please confirm the brokers correct usernname and password.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::mqtt::config::step::broker::data_description::username%]", + "password": "[%key:component::mqtt::config::step::broker::data_description::password%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "error": { @@ -84,6 +97,7 @@ "bad_client_cert_key": "Client certificate and private key are not a valid pair", "bad_ws_headers": "Supply valid HTTP headers as a JSON object", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_inclusion": "The client certificate and private key must be configurered together" } }, @@ -264,10 +278,10 @@ "message": "Unable to publish: topic template `{topic_template}` produced an invalid topic `{topic}` after rendering ({error})" }, "mqtt_not_setup_cannot_subscribe": { - "message": "Cannot subscribe to topic '{topic}', make sure MQTT is set up correctly." + "message": "Cannot subscribe to topic \"{topic}\", make sure MQTT is set up correctly." }, "mqtt_not_setup_cannot_publish": { - "message": "Cannot publish to topic '{topic}', make sure MQTT is set up correctly." + "message": "Cannot publish to topic \"{topic}\", make sure MQTT is set up correctly." } } } diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index d7086885b24..7aa798a7a3c 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -99,7 +99,6 @@ async def async_attach_trigger( "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload ) - remove = await mqtt.async_subscribe( + return await mqtt.async_subscribe( hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos ) - return remove diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index a4635d1e4cc..ab21ab56f1b 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -218,8 +218,7 @@ def valid_birth_will(config: ConfigType) -> ConfigType: def get_mqtt_data(hass: HomeAssistant) -> MqttData: """Return typed MqttData from hass.data[DATA_MQTT].""" - mqtt_data: MqttData - mqtt_data = hass.data[DATA_MQTT] + mqtt_data: MqttData = hass.data[DATA_MQTT] return mqtt_data diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index a9b86c4bf8f..3a0953a0158 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -1,16 +1,14 @@ """Publish simple item state changes via MQTT.""" -from collections.abc import Mapping import json import logging -from typing import Any import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import valid_publish_topic from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -57,9 +55,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not base_topic.endswith("/"): base_topic = f"{base_topic}/" - async def _state_publisher(evt: Event) -> None: - entity_id: str = evt.data["entity_id"] - new_state: State = evt.data["new_state"] + async def _state_publisher(evt: Event[EventStateChangedData]) -> None: + entity_id = evt.data["entity_id"] + new_state = evt.data["new_state"] + assert new_state payload = new_state.state @@ -92,9 +91,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def _ha_started(hass: HomeAssistant) -> None: @callback - def _event_filter(event_data: Mapping[str, Any]) -> bool: - entity_id: str = event_data["entity_id"] - new_state: State | None = event_data["new_state"] + def _event_filter(event_data: EventStateChangedData) -> bool: + entity_id = event_data["entity_id"] + new_state = 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 d6ebdf3e711..d1118ed7ab5 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -37,8 +37,8 @@ def get_service( try: return MSTeamsNotificationService(webhook_url) - except RuntimeError as err: - _LOGGER.exception("Error in creating a new Microsoft Teams message: %s", err) + except RuntimeError: + _LOGGER.exception("Error in creating a new Microsoft Teams message") return None diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index 55957f160a3..0ffcc11c97e 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -1,5 +1,7 @@ """Config flow for Mullvad VPN integration.""" +from typing import Any + from mullvad_api import MullvadAPI, MullvadAPIError from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -12,10 +14,10 @@ class MullvadConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None) -> ConfigFlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" - self._async_abort_entries_match() - errors = {} if user_input is not None: try: diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json index 13dd27375cf..fc3faefe1e3 100644 --- a/homeassistant/components/mullvad/manifest.json +++ b/homeassistant/components/mullvad/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mullvad", "iot_class": "cloud_polling", - "requirements": ["mullvad-api==1.0.0"] + "requirements": ["mullvad-api==1.0.0"], + "single_config_entry": true } diff --git a/homeassistant/components/mullvad/strings.json b/homeassistant/components/mullvad/strings.json index 3e029184155..d3f757e829c 100644 --- a/homeassistant/components/mullvad/strings.json +++ b/homeassistant/components/mullvad/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index b4347a39e12..9a8d79ca3a7 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -63,7 +63,7 @@ def is_persistence_file(value: str) -> str: def _get_schema_common(user_input: dict[str, str]) -> dict: """Create a schema with options common to all gateway types.""" - schema = { + return { vol.Required( CONF_VERSION, description={ @@ -72,7 +72,6 @@ def _get_schema_common(user_input: dict[str, str]) -> dict: ): str, vol.Optional(CONF_PERSISTENCE_FILE): str, } - return schema def _validate_version(version: str) -> dict[str, str]: diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index b932a33d0fa..11f27f8a108 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -10,7 +10,7 @@ import socket import sys from typing import Any -from mysensors import BaseAsyncGateway, Message, Sensor, mysensors +from mysensors import BaseAsyncGateway, Message, Sensor, get_const, mysensors import voluptuous as vol from homeassistant.components.mqtt import ( @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( @@ -71,9 +72,9 @@ def is_socket_address(value: str) -> str: """Validate that value is a valid address.""" try: socket.getaddrinfo(value, None) - return value except OSError as err: raise vol.Invalid("Device is not a valid domain name or ip address") from err + return value async def try_connect( @@ -129,7 +130,7 @@ async def setup_gateway( ) -> BaseAsyncGateway | None: """Set up the Gateway for the given ConfigEntry.""" - ready_gateway = await _get_gateway( + return await _get_gateway( hass, gateway_type=entry.data[CONF_GATEWAY_TYPE], device=entry.data[CONF_DEVICE], @@ -144,7 +145,6 @@ async def setup_gateway( topic_out_prefix=entry.data.get(CONF_TOPIC_OUT_PREFIX), retain=entry.data.get(CONF_RETAIN, False), ) - return ready_gateway async def _get_gateway( @@ -163,6 +163,12 @@ async def _get_gateway( ) -> BaseAsyncGateway | None: """Return gateway after setup of the gateway.""" + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # get_const will import a const module based on the version + # so we need to import it here to avoid it being imported + # in the event loop + await hass.async_add_import_executor_job(get_const, version) + if persistence_file is not None: # Interpret relative paths to be in hass config folder. # Absolute paths will be left as they are. diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index cb075b8f485..c456cfd1f11 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -152,7 +152,7 @@ def get_child_schema( """Return a child schema.""" set_req = gateway.const.SetReq child_schema = child.get_schema(gateway.protocol_version) - schema = child_schema.extend( + return child_schema.extend( { vol.Required( set_req[name].value, msg=invalid_msg(gateway, child, name) @@ -161,7 +161,6 @@ def get_child_schema( }, extra=vol.ALLOW_EXTRA, ) - return schema def invalid_msg( diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 2201eb778d6..66ea2cc9679 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -38,8 +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[KEY_HASS], request.query) - return res + return await self._handle(request.app[KEY_HASS], request.query) async def _handle(self, hass, data): """Handle requests to the myStrom endpoint.""" diff --git a/homeassistant/components/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py index 38b8c9c5fd3..6b7ec66a7b4 100644 --- a/homeassistant/components/myuplink/binary_sensor.py +++ b/homeassistant/components/myuplink/binary_sensor.py @@ -34,11 +34,7 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") - description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( - device_point.parameter_id - ) - - return description + return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) async def async_setup_entry( diff --git a/homeassistant/components/myuplink/diagnostics.py b/homeassistant/components/myuplink/diagnostics.py index d108db595a1..15b643ffd92 100644 --- a/homeassistant/components/myuplink/diagnostics.py +++ b/homeassistant/components/myuplink/diagnostics.py @@ -39,9 +39,7 @@ async def async_get_config_entry_diagnostics( } ) - diagnostics_data = { + return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "myuplink_data": async_redact_data(myuplink_data, TO_REDACT), } - - return diagnostics_data diff --git a/homeassistant/components/myuplink/strings.json b/homeassistant/components/myuplink/strings.json index f01bb1990cc..2efc0d05b34 100644 --- a/homeassistant/components/myuplink/strings.json +++ b/homeassistant/components/myuplink/strings.json @@ -12,12 +12,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py index d26695f4cbe..11dca1e2ac0 100644 --- a/homeassistant/components/myuplink/switch.py +++ b/homeassistant/components/myuplink/switch.py @@ -39,11 +39,7 @@ def get_description(device_point: DevicePoint) -> SwitchEntityDescription | None 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") - description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( - device_point.parameter_id - ) - - return description + return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) async def async_setup_entry( diff --git a/homeassistant/components/nam/diagnostics.py b/homeassistant/components/nam/diagnostics.py index 8ce885f0297..db1a97d8fb1 100644 --- a/homeassistant/components/nam/diagnostics.py +++ b/homeassistant/components/nam/diagnostics.py @@ -22,9 +22,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "info": async_redact_data(config_entry.data, TO_REDACT), "data": asdict(coordinator.data), } - - return diagnostics_data diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index a4ef9af9aee..7b1c584c293 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["nettigo_air_monitor"], "quality_scale": "platinum", - "requirements": ["nettigo-air-monitor==2.2.2"], + "requirements": ["nettigo-air-monitor==3.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index f269e3e89d6..33793fe836b 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -73,7 +73,7 @@ class NestDeviceInfo: """Return device suggested area based on the Google Home room.""" if parent_relations := self._device.parent_relations: items = sorted(parent_relations.items()) - names = [name for id, name in items] + names = [name for _, name in items] return " ".join(names) return None diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 89244642207..354066e2d87 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==3.0.3"] + "requirements": ["google-nest-sdm==3.0.4"] } diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 322af8cf3ac..f402009e13b 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.start import async_at_started @@ -243,3 +244,20 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) except cloud.CloudNotAvailable: pass + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + data = hass.data[DOMAIN][config_entry.entry_id][DATA_HANDLER] + modules = [m for h in data.account.homes.values() for m in h.modules] + rooms = [r for h in data.account.homes.values() for r in h.rooms] + + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + and identifier[1] in modules + or identifier[1] in rooms + ) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py new file mode 100644 index 00000000000..c478525753a --- /dev/null +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -0,0 +1,60 @@ +"""Support for Netatmo binary sensors.""" + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import NETATMO_CREATE_WEATHER_SENSOR +from .data_handler import NetatmoDevice +from .entity import NetatmoWeatherModuleEntity + +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="reachable", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Netatmo binary sensors based on a config entry.""" + + @callback + def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None: + async_add_entities( + NetatmoWeatherBinarySensor(netatmo_device, description) + for description in BINARY_SENSOR_TYPES + if description.key in netatmo_device.device.features + ) + + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity + ) + ) + + +class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity): + """Implementation of a Netatmo binary sensor.""" + + def __init__( + self, device: NetatmoDevice, description: BinarySensorEntityDescription + ) -> None: + """Initialize a Netatmo binary sensor.""" + super().__init__(device) + self.entity_description = description + self._attr_unique_id = f"{self.device.entity_id}-{description.key}" + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_on = self.device.reachable + self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index bd12e757359..3bd7bcd859d 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -40,7 +40,7 @@ from .const import ( WEBHOOK_PUSH_TYPE, ) from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -80,12 +80,16 @@ async def async_setup_entry( ) -class NetatmoCamera(NetatmoBaseEntity, Camera): +class NetatmoCamera(NetatmoModuleEntity, Camera): """Representation of a Netatmo camera.""" _attr_brand = MANUFACTURER - _attr_has_entity_name = True _attr_supported_features = CameraEntityFeature.STREAM + _attr_configuration_url = CONF_URL_SECURITY + device: NaModules.Camera + _quality = DEFAULT_QUALITY + _monitoring: bool | None = None + _attr_name = None def __init__( self, @@ -93,30 +97,22 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): ) -> None: """Set up for access to the Netatmo camera images.""" Camera.__init__(self) - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) - self._camera = cast(NaModules.Camera, netatmo_device.device) - self._id = self._camera.entity_id - self._home_id = self._camera.home.entity_id - self._device_name = self._camera.name - self._model = self._camera.device_type - self._config_url = CONF_URL_SECURITY - self._attr_unique_id = f"{self._id}-{self._model}" - self._quality = DEFAULT_QUALITY - self._monitoring: bool | None = None + self._attr_unique_id = f"{netatmo_device.device.entity_id}-{self.device_type}" self._light_state = None self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, - SIGNAL_NAME: f"{HOME}-{self._home_id}", + "home_id": self.home.entity_id, + SIGNAL_NAME: f"{HOME}-{self.home.entity_id}", }, { "name": EVENT, - "home_id": self._home_id, - SIGNAL_NAME: f"{EVENT}-{self._home_id}", + "home_id": self.home.entity_id, + SIGNAL_NAME: f"{EVENT}-{self.home.entity_id}", }, ] ) @@ -134,7 +130,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): ) ) - self.hass.data[DOMAIN][DATA_CAMERAS][self._id] = self._device_name + self.hass.data[DOMAIN][DATA_CAMERAS][self.device.entity_id] = self.device.name @callback def handle_event(self, event: dict) -> None: @@ -144,7 +140,10 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): if not data.get("camera_id"): return - if data["home_id"] == self._home_id and data["camera_id"] == self._id: + if ( + data["home_id"] == self.home.entity_id + and data["camera_id"] == self.device.entity_id + ): if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): self._attr_is_streaming = False self._monitoring = False @@ -168,7 +167,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): ) -> bytes | None: """Return a still image response from the camera.""" try: - return cast(bytes, await self._camera.async_get_live_snapshot()) + return cast(bytes, await self.device.async_get_live_snapshot()) except ( aiohttp.ClientPayloadError, aiohttp.ContentTypeError, @@ -183,50 +182,50 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): def supported_features(self) -> CameraEntityFeature: """Return supported features.""" supported_features = CameraEntityFeature.ON_OFF - if self._model != "NDB": + if self.device_type != "NDB": supported_features |= CameraEntityFeature.STREAM return supported_features async def async_turn_off(self) -> None: """Turn off camera.""" - await self._camera.async_monitoring_off() + await self.device.async_monitoring_off() async def async_turn_on(self) -> None: """Turn on camera.""" - await self._camera.async_monitoring_on() + await self.device.async_monitoring_on() async def stream_source(self) -> str: """Return the stream source.""" - if self._camera.is_local: - await self._camera.async_update_camera_urls() + if self.device.is_local: + await self.device.async_update_camera_urls() - if self._camera.local_url: - return f"{self._camera.local_url}/live/files/{self._quality}/index.m3u8" - return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8" + if self.device.local_url: + return f"{self.device.local_url}/live/files/{self._quality}/index.m3u8" + return f"{self.device.vpn_url}/live/files/{self._quality}/index.m3u8" @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_on = self._camera.alim_status is not None - self._attr_available = self._camera.alim_status is not None + self._attr_is_on = self.device.alim_status is not None + self._attr_available = self.device.alim_status is not None - if self._camera.monitoring is not None: - self._attr_is_streaming = self._camera.monitoring - self._attr_motion_detection_enabled = self._camera.monitoring + if self.device.monitoring is not None: + self._attr_is_streaming = self.device.monitoring + self._attr_motion_detection_enabled = self.device.monitoring - self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( - self._camera.events + self.hass.data[DOMAIN][DATA_EVENTS][self.device.entity_id] = ( + self.process_events(self.device.events) ) self._attr_extra_state_attributes.update( { - "id": self._id, + "id": self.device.entity_id, "monitoring": self._monitoring, - "sd_status": self._camera.sd_status, - "alim_status": self._camera.alim_status, - "is_local": self._camera.is_local, - "vpn_url": self._camera.vpn_url, - "local_url": self._camera.local_url, + "sd_status": self.device.sd_status, + "alim_status": self.device.alim_status, + "is_local": self.device.is_local, + "vpn_url": self.device.vpn_url, + "local_url": self.device.local_url, "light_state": self._light_state, } ) @@ -249,9 +248,9 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): def get_video_url(self, video_id: str) -> str: """Get video url.""" - if self._camera.is_local: - return f"{self._camera.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8" - return f"{self._camera.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + if self.device.is_local: + return f"{self.device.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + return f"{self.device.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8" def fetch_person_ids(self, persons: list[str | None]) -> list[str]: """Fetch matching person ids for given list of persons.""" @@ -260,7 +259,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): for person in persons: person_id = None - for pid, data in self._camera.home.persons.items(): + for pid, data in self.home.persons.items(): if data.pseudo == person: person_ids.append(pid) person_id = pid @@ -279,7 +278,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): persons = kwargs.get(ATTR_PERSONS, []) person_ids = self.fetch_person_ids(persons) - await self._camera.home.async_set_persons_home(person_ids=person_ids) + await self.home.async_set_persons_home(person_ids=person_ids) _LOGGER.debug("Set %s as at home", persons) async def _service_set_person_away(self, **kwargs: Any) -> None: @@ -288,7 +287,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): person_ids = self.fetch_person_ids([person] if person else []) person_id = next(iter(person_ids), None) - await self._camera.home.async_set_persons_away( + await self.home.async_set_persons_away( person_id=person_id, ) @@ -299,11 +298,11 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): async def _service_set_camera_light(self, **kwargs: Any) -> None: """Service to set light mode.""" - if not isinstance(self._camera, NaModules.netatmo.NOC): + if not isinstance(self.device, NaModules.netatmo.NOC): raise HomeAssistantError( - f"{self._model} <{self._device_name}> does not have a floodlight" + f"{self.device_type} <{self.device.name}> does not have a floodlight" ) mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE)) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) - await self._camera.async_set_floodlight_state(mode) + await self.device.async_set_floodlight_state(mode) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 15bf3291618..e257c7a89ea 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -22,7 +22,6 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_SUGGESTED_AREA, ATTR_TEMPERATURE, PRECISION_HALVES, STATE_OFF, @@ -30,7 +29,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -42,7 +40,6 @@ from .const import ( ATTR_SELECTED_SCHEDULE, ATTR_TARGET_TEMPERATURE, ATTR_TIME_PERIOD, - CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, EVENT_TYPE_CANCEL_SET_POINT, @@ -57,7 +54,7 @@ from .const import ( SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom -from .entity import NetatmoBaseEntity +from .entity import NetatmoRoomEntity _LOGGER = logging.getLogger(__name__) @@ -182,7 +179,7 @@ async def async_setup_entry( ) -class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): +class NetatmoThermostat(NetatmoRoomEntity, ClimateEntity): """Representation a Netatmo thermostat.""" _attr_hvac_mode = HVACMode.AUTO @@ -191,47 +188,37 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_name = None + _away: bool | None = None + _connected: bool | None = None _enable_turn_on_off_backwards_compatibility = False - def __init__(self, netatmo_device: NetatmoRoom) -> None: + _away_temperature: float | None = None + _hg_temperature: float | None = None + _boilerstatus: bool | None = None + + def __init__(self, room: NetatmoRoom) -> None: """Initialize the sensor.""" - ClimateEntity.__init__(self) - super().__init__(netatmo_device.data_handler) + super().__init__(room) - self._room = netatmo_device.room - self._id = self._room.entity_id - self._home_id = self._room.home.entity_id - - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._room.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - assert self._room.climate_type - self._model: DeviceType = self._room.climate_type - - self._config_url = CONF_URL_ENERGY - - self._attr_name = self._room.name - self._away: bool | None = None - self._connected: bool | None = None - - self._away_temperature: float | None = None - self._hg_temperature: float | None = None - self._boilerstatus: bool | None = None self._selected_schedule = None self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT] - if self._model is NA_THERM: + if self.device_type is NA_THERM: self._attr_hvac_modes.append(HVACMode.OFF) - self._attr_unique_id = f"{self._room.entity_id}-{self._model}" + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" async def async_added_to_hass(self) -> None: """Entity created.""" @@ -256,12 +243,12 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): """Handle webhook events.""" data = event["data"] - if self._room.home.entity_id != data["home_id"]: + if self.home.entity_id != data["home_id"]: return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: self._selected_schedule = getattr( - self.hass.data[DOMAIN][DATA_SCHEDULES][self._room.home.entity_id].get( + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( data["schedule_id"] ), "name", @@ -276,7 +263,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): home = data["home"] - if self._room.home.entity_id != home["id"]: + if self.home.entity_id != home["id"]: return if data["event_type"] == EVENT_TYPE_THERM_MODE: @@ -295,7 +282,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): for room in home.get("rooms", []): if ( data["event_type"] == EVENT_TYPE_SET_POINT - and self._room.entity_id == room["id"] + and self.device.entity_id == room["id"] ): if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: self._attr_hvac_mode = HVACMode.OFF @@ -317,7 +304,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): if ( data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT - and self._room.entity_id == room["id"] + and self.device.entity_id == room["id"] ): if self._attr_hvac_mode == HVACMode.OFF: self._attr_hvac_mode = HVACMode.AUTO @@ -330,11 +317,11 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): @property def hvac_action(self) -> HVACAction: """Return the current running hvac operation if supported.""" - if self._model != NA_VALVE and self._boilerstatus is not None: + if self.device_type != NA_VALVE and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve if ( - heating_req := getattr(self._room, "heating_power_request", 0) + heating_req := getattr(self.device, "heating_power_request", 0) ) is not None and heating_req > 0: return HVACAction.HEATING return HVACAction.IDLE @@ -352,16 +339,17 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): """Set new preset mode.""" if ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) - and self._model == NA_VALVE + and self.device_type == NA_VALVE and self._attr_hvac_mode == HVACMode.HEAT ): - await self._room.async_therm_set( + await self.device.async_therm_set( STATE_NETATMO_HOME, ) elif ( - preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE + preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) + and self.device_type == NA_VALVE ): - await self._room.async_therm_set( + await self.device.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) @@ -369,11 +357,11 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._attr_hvac_mode == HVACMode.HEAT ): - await self._room.async_therm_set(STATE_NETATMO_HOME) + await self.device.async_therm_set(STATE_NETATMO_HOME) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): - await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) + await self.device.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) elif preset_mode in THERM_MODES: - await self._room.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) + await self.device.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -381,25 +369,25 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature for 2 hours.""" - await self._room.async_therm_set( + await self.device.async_therm_set( STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP) ) self.async_write_ha_state() async def async_turn_off(self) -> None: """Turn the entity off.""" - if self._model == NA_VALVE: - await self._room.async_therm_set( + if self.device_type == NA_VALVE: + await self.device.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) elif self._attr_hvac_mode != HVACMode.OFF: - await self._room.async_therm_set(STATE_NETATMO_OFF) + await self.device.async_therm_set(STATE_NETATMO_OFF) self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the entity on.""" - await self._room.async_therm_set(STATE_NETATMO_HOME) + await self.device.async_therm_set(STATE_NETATMO_HOME) self.async_write_ha_state() @property @@ -410,36 +398,36 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if not self._room.reachable: + if not self.device.reachable: if self.available: self._connected = False return self._connected = True - self._away_temperature = self._room.home.get_away_temp() - self._hg_temperature = self._room.home.get_hg_temp() - self._attr_current_temperature = self._room.therm_measured_temperature - self._attr_target_temperature = self._room.therm_setpoint_temperature + self._away_temperature = self.home.get_away_temp() + self._hg_temperature = self.home.get_hg_temp() + self._attr_current_temperature = self.device.therm_measured_temperature + self._attr_target_temperature = self.device.therm_setpoint_temperature self._attr_preset_mode = NETATMO_MAP_PRESET[ - getattr(self._room, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE) + getattr(self.device, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE) ] self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] self._away = self._attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] self._selected_schedule = getattr( - self._room.home.get_selected_schedule(), "name", None + self.home.get_selected_schedule(), "name", None ) self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( self._selected_schedule ) - if self._model == NA_VALVE: + if self.device_type == NA_VALVE: self._attr_extra_state_attributes[ATTR_HEATING_POWER_REQUEST] = ( - self._room.heating_power_request + self.device.heating_power_request ) else: - for module in self._room.modules.values(): + for module in self.device.modules.values(): if hasattr(module, "boiler_status"): module = cast(NATherm1, module) if module.boiler_status is not None: @@ -450,7 +438,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._room.home.entity_id + self.home.entity_id ].items(): if schedule.name == schedule_name: schedule_id = sid @@ -460,10 +448,10 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - await self._room.home.async_switch_schedule(schedule_id=schedule_id) + await self.home.async_switch_schedule(schedule_id=schedule_id) _LOGGER.debug( "Setting %s schedule to %s (%s)", - self._room.home.entity_id, + self.home.entity_id, kwargs.get(ATTR_SCHEDULE_NAME), schedule_id, ) @@ -475,12 +463,12 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): end_datetime = kwargs[ATTR_END_DATETIME] end_timestamp = int(dt_util.as_timestamp(end_datetime)) - await self._room.home.async_set_thermmode( + await self.home.async_set_thermmode( mode=PRESET_MAP_NETATMO[preset_mode], end_time=end_timestamp ) _LOGGER.debug( "Setting %s preset to %s with end datetime %s", - self._room.home.entity_id, + self.home.entity_id, preset_mode, end_timestamp, ) @@ -494,11 +482,11 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _LOGGER.debug( "Setting %s to target temperature %s with end datetime %s", - self._room.entity_id, + self.device.entity_id, target_temperature, end_timestamp, ) - await self._room.async_therm_manual(target_temperature, end_timestamp) + await self.device.async_therm_manual(target_temperature, end_timestamp) async def _async_service_set_temperature_with_time_period( self, **kwargs: Any @@ -508,22 +496,15 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _LOGGER.debug( "Setting %s to target temperature %s with time period %s", - self._room.entity_id, + self.device.entity_id, target_temperature, time_period, ) now_timestamp = dt_util.as_timestamp(dt_util.utcnow()) end_timestamp = int(now_timestamp + time_period.seconds) - await self._room.async_therm_manual(target_temperature, end_timestamp) + await self.device.async_therm_manual(target_temperature, end_timestamp) async def _async_service_clear_temperature_setting(self, **kwargs: Any) -> None: - _LOGGER.debug("Clearing %s temperature setting", self._room.entity_id) - await self._room.async_therm_home() - - @property - def device_info(self) -> DeviceInfo: - """Return the device info for the thermostat.""" - device_info: DeviceInfo = super().device_info - device_info[ATTR_SUGGESTED_AREA] = self._room.name - return device_info + _LOGGER.debug("Clearing %s temperature setting", self.device.entity_id) + await self.device.async_therm_home() diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 34a5c42038e..74f2ebc84b2 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -9,9 +9,11 @@ MANUFACTURER = "Netatmo" DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.CAMERA, Platform.CLIMATE, Platform.COVER, + Platform.FAN, Platform.LIGHT, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/netatmo/cover.py b/homeassistant/components/netatmo/cover.py index f2b5c801eec..c34b3a1b47b 100644 --- a/homeassistant/components/netatmo/cover.py +++ b/homeassistant/components/netatmo/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from pyatmo import modules as NaModules @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class NetatmoCover(NetatmoBaseEntity, CoverEntity): +class NetatmoCover(NetatmoModuleEntity, CoverEntity): """Representation of a Netatmo cover device.""" _attr_supported_features = ( @@ -52,56 +52,51 @@ class NetatmoCover(NetatmoBaseEntity, CoverEntity): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) + _attr_configuration_url = CONF_URL_CONTROL _attr_device_class = CoverDeviceClass.SHUTTER + _attr_name = None + device: NaModules.Shutter def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize the Netatmo device.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) - self._cover = cast(NaModules.Shutter, netatmo_device.device) + self._attr_is_closed = self.device.current_position == 0 - self._id = self._cover.entity_id - self._attr_name = self._device_name = self._cover.name - self._model = self._cover.device_type - self._config_url = CONF_URL_CONTROL - - self._home_id = self._cover.home.entity_id - self._attr_is_closed = self._cover.current_position == 0 - - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._cover.async_close() + await self.device.async_close() self._attr_is_closed = True self.async_write_ha_state() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._cover.async_open() + await self.device.async_open() self._attr_is_closed = False self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - await self._cover.async_stop() + await self.device.async_stop() async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover shutter to a specific position.""" - await self._cover.async_set_target_position(kwargs[ATTR_POSITION]) + await self.device.async_set_target_position(kwargs[ATTR_POSITION]) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_closed = self._cover.current_position == 0 - self._attr_current_cover_position = self._cover.current_position + self._attr_is_closed = self.device.current_position == 0 + self._attr_current_cover_position = self.device.current_position diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index 579d2177824..6fdebcf0c3f 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -2,39 +2,40 @@ from __future__ import annotations -from typing import Any +from abc import abstractmethod +from typing import Any, cast -from pyatmo import DeviceType -from pyatmo.modules.device_types import ( - DEVICE_DESCRIPTION_MAP, - DeviceType as NetatmoDeviceType, -) +from pyatmo import DeviceType, Home, Module, Room +from pyatmo.modules.base_class import NetatmoBase, Place +from pyatmo.modules.device_types import DEVICE_DESCRIPTION_MAP +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity -from .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME -from .data_handler import PUBLIC, NetatmoDataHandler +from .const import ( + CONF_URL_ENERGY, + CONF_URL_WEATHER, + DATA_DEVICE_IDS, + DEFAULT_ATTRIBUTION, + DOMAIN, + SIGNAL_NAME, +) +from .data_handler import PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom class NetatmoBaseEntity(Entity): """Netatmo entity base class.""" _attr_attribution = DEFAULT_ATTRIBUTION + _attr_has_entity_name = True def __init__(self, data_handler: NetatmoDataHandler) -> None: """Set up Netatmo entity base.""" self.data_handler = data_handler self._publishers: list[dict[str, Any]] = [] - - self._device_name: str = "" - self._id: str = "" - self._model: DeviceType - self._config_url: str | None = None - self._attr_name = None - self._attr_unique_id = None self._attr_extra_state_attributes = {} async def async_added_to_hass(self) -> None: @@ -72,10 +73,6 @@ class NetatmoBaseEntity(Entity): ): await self.data_handler.unsubscribe(signal_name, None) - registry = dr.async_get(self.hass) - if device := registry.async_get_device(identifiers={(DOMAIN, self._id)}): - self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id - self.async_update_callback() async def async_will_remove_from_hass(self) -> None: @@ -92,18 +89,118 @@ class NetatmoBaseEntity(Entity): """Update the entity's state.""" raise NotImplementedError + +class NetatmoDeviceEntity(NetatmoBaseEntity): + """Netatmo entity base class.""" + + def __init__(self, data_handler: NetatmoDataHandler, device: NetatmoBase) -> None: + """Set up Netatmo entity base.""" + super().__init__(data_handler) + self.device = device + @property - def device_info(self) -> DeviceInfo: - """Return the device info for the sensor.""" - if "." in self._model: - netatmo_device = NetatmoDeviceType(self._model.partition(".")[2]) - else: - netatmo_device = getattr(NetatmoDeviceType, self._model) - manufacturer, model = DEVICE_DESCRIPTION_MAP[netatmo_device] - return DeviceInfo( - configuration_url=self._config_url, - identifiers={(DOMAIN, self._id)}, - name=self._device_name, - manufacturer=manufacturer, - model=model, + @abstractmethod + def device_type(self) -> DeviceType: + """Return the device type.""" + + @property + def device_description(self) -> tuple[str, str]: + """Return the model of this device.""" + return DEVICE_DESCRIPTION_MAP[self.device_type] + + @property + def home(self) -> Home: + """Return the home this room belongs to.""" + return self.device.home + + +class NetatmoRoomEntity(NetatmoDeviceEntity): + """Netatmo room entity base class.""" + + device: Room + + def __init__(self, room: NetatmoRoom) -> None: + """Set up a Netatmo room entity.""" + super().__init__(room.data_handler, room.room) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, room.room.entity_id)}, + name=room.room.name, + manufacturer=self.device_description[0], + model=self.device_description[1], + configuration_url=CONF_URL_ENERGY, + suggested_area=room.room.name, ) + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + registry = dr.async_get(self.hass) + if device := registry.async_get_device( + identifiers={(DOMAIN, self.device.entity_id)} + ): + self.hass.data[DOMAIN][DATA_DEVICE_IDS][self.device.entity_id] = device.id + + @property + def device_type(self) -> DeviceType: + """Return the device type.""" + assert self.device.climate_type + return self.device.climate_type + + +class NetatmoModuleEntity(NetatmoDeviceEntity): + """Netatmo module entity base class.""" + + device: Module + _attr_configuration_url: str + + def __init__(self, device: NetatmoDevice) -> None: + """Set up a Netatmo module entity.""" + super().__init__(device.data_handler, device.device) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.device.entity_id)}, + name=device.device.name, + manufacturer=self.device_description[0], + model=self.device_description[1], + configuration_url=self._attr_configuration_url, + ) + + @property + def device_type(self) -> DeviceType: + """Return the device type.""" + return self.device.device_type + + +class NetatmoWeatherModuleEntity(NetatmoModuleEntity): + """Netatmo weather module entity base class.""" + + _attr_configuration_url = CONF_URL_WEATHER + + def __init__(self, device: NetatmoDevice) -> None: + """Set up a Netatmo weather module entity.""" + super().__init__(device) + category = getattr(self.device.device_category, "name") + self._publishers.extend( + [ + { + "name": category, + SIGNAL_NAME: category, + }, + ] + ) + + if hasattr(self.device, "place"): + place = cast(Place, getattr(self.device, "place")) + if hasattr(place, "location") and place.location is not None: + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: place.location.latitude, + ATTR_LONGITUDE: place.location.longitude, + } + ) + + @property + def device_type(self) -> DeviceType: + """Return the Netatmo device type.""" + if "." not in self.device.device_type: + return super().device_type + return DeviceType(self.device.device_type.partition(".")[2]) diff --git a/homeassistant/components/netatmo/fan.py b/homeassistant/components/netatmo/fan.py index 1b2798dd118..71a8c548622 100644 --- a/homeassistant/components/netatmo/fan.py +++ b/homeassistant/components/netatmo/fan.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Final, cast +from typing import Final from pyatmo import modules as NaModules @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_URL_CONTROL, NETATMO_CREATE_FAN from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -43,46 +43,38 @@ async def async_setup_entry( ) -class NetatmoFan(NetatmoBaseEntity, FanEntity): +class NetatmoFan(NetatmoModuleEntity, FanEntity): """Representation of a Netatmo fan.""" _attr_preset_modes = ["slow", "fast"] _attr_supported_features = FanEntityFeature.PRESET_MODE + _attr_configuration_url = CONF_URL_CONTROL + _attr_name = None + device: NaModules.Fan def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize of Netatmo fan.""" - super().__init__(netatmo_device.data_handler) - - self._fan = cast(NaModules.Fan, netatmo_device.device) - - self._id = self._fan.entity_id - self._attr_name = self._device_name = self._fan.name - self._model = self._fan.device_type - self._config_url = CONF_URL_CONTROL - - self._home_id = self._fan.home.entity_id - - self._signal_name = f"{HOME}-{self._home_id}" + super().__init__(netatmo_device) self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, - SIGNAL_NAME: self._signal_name, + "home_id": self.home.entity_id, + SIGNAL_NAME: f"{HOME}-{self.home.entity_id}", }, ] ) - self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - await self._fan.async_set_fan_speed(PRESET_MAPPING[preset_mode]) + await self.device.async_set_fan_speed(PRESET_MAPPING[preset_mode]) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if self._fan.fan_speed is None: + if self.device.fan_speed is None: self._attr_preset_mode = None return - self._attr_preset_mode = PRESETS.get(self._fan.fan_speed) + self._attr_preset_mode = PRESETS.get(self.device.fan_speed) diff --git a/homeassistant/components/netatmo/icons.json b/homeassistant/components/netatmo/icons.json index c585a9c7587..31b1740ab21 100644 --- a/homeassistant/components/netatmo/icons.json +++ b/homeassistant/components/netatmo/icons.json @@ -1,4 +1,38 @@ { + "entity": { + "sensor": { + "temp_trend": { + "default": "mdi:trending-up" + }, + "pressure_trend": { + "default": "mdi:trending-up" + }, + "wind_direction": { + "default": "mdi:compass-outline" + }, + "wind_angle": { + "default": "mdi:compass-outline" + }, + "gust_direction": { + "default": "mdi:compass-outline" + }, + "gust_angle": { + "default": "mdi:compass-outline" + }, + "reachable": { + "default": "mdi:signal" + }, + "rf_strength": { + "default": "mdi:signal" + }, + "wifi_strength": { + "default": "mdi:wifi" + }, + "health_idx": { + "default": "mdi:cloud" + } + } + }, "services": { "set_camera_light": "mdi:led-on", "set_schedule": "mdi:calendar-clock", diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 9ccab51f792..b1871e9dabb 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from pyatmo import modules as NaModules @@ -24,7 +24,7 @@ from .const import ( WEBHOOK_PUSH_TYPE, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -62,36 +62,28 @@ async def async_setup_entry( ) -class NetatmoCameraLight(NetatmoBaseEntity, LightEntity): +class NetatmoCameraLight(NetatmoModuleEntity, LightEntity): """Representation of a Netatmo Presence camera light.""" + device: NaModules.NOC + _attr_is_on = False + _attr_name = None + _attr_configuration_url = CONF_URL_SECURITY _attr_color_mode = ColorMode.ONOFF _attr_has_entity_name = True _attr_supported_color_modes = {ColorMode.ONOFF} - def __init__( - self, - netatmo_device: NetatmoDevice, - ) -> None: + def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize a Netatmo Presence camera light.""" - LightEntity.__init__(self) - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) + self._attr_unique_id = f"{self.device.entity_id}-light" - self._camera = cast(NaModules.NOC, netatmo_device.device) - self._id = self._camera.entity_id - self._home_id = self._camera.home.entity_id - self._device_name = self._camera.name - self._model = self._camera.device_type - self._config_url = CONF_URL_SECURITY - self._is_on = False - self._attr_unique_id = f"{self._id}-light" - - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._camera.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] @@ -118,11 +110,11 @@ class NetatmoCameraLight(NetatmoBaseEntity, LightEntity): return if ( - data["home_id"] == self._home_id - and data["camera_id"] == self._id + data["home_id"] == self.home.entity_id + and data["camera_id"] == self.device.entity_id and data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE ): - self._is_on = bool(data["sub_type"] == "on") + self._attr_is_on = bool(data["sub_type"] == "on") self.async_write_ha_state() return @@ -132,59 +124,47 @@ class NetatmoCameraLight(NetatmoBaseEntity, LightEntity): """If the webhook is not established, mark as unavailable.""" return bool(self.data_handler.webhook) - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._is_on - async def async_turn_on(self, **kwargs: Any) -> None: """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self.name) - await self._camera.async_floodlight_on() + await self.device.async_floodlight_on() async def async_turn_off(self, **kwargs: Any) -> None: """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self.name) - await self._camera.async_floodlight_auto() + await self.device.async_floodlight_auto() @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._is_on = bool(self._camera.floodlight == "on") + self._attr_is_on = bool(self.device.floodlight == "on") -class NetatmoLight(NetatmoBaseEntity, LightEntity): +class NetatmoLight(NetatmoModuleEntity, LightEntity): """Representation of a dimmable light by Legrand/BTicino.""" - def __init__( - self, - netatmo_device: NetatmoDevice, - ) -> None: + _attr_name = None + _attr_configuration_url = CONF_URL_CONTROL + _attr_brightness: int | None = 0 + device: NaModules.NLFN + + def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize a Netatmo light.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) + self._attr_unique_id = f"{self.device.entity_id}-light" - self._dimmer = cast(NaModules.NLFN, netatmo_device.device) - self._id = self._dimmer.entity_id - self._home_id = self._dimmer.home.entity_id - self._device_name = self._dimmer.name - self._attr_name = f"{self._device_name}" - self._model = self._dimmer.device_type - self._config_url = CONF_URL_CONTROL - self._attr_brightness = 0 - self._attr_unique_id = f"{self._id}-light" - - if self._dimmer.brightness is not None: + if self.device.brightness is not None: self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_color_mode = ColorMode.ONOFF self._attr_supported_color_modes = {self._attr_color_mode} - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._dimmer.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] @@ -193,27 +173,27 @@ class NetatmoLight(NetatmoBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: - await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) + await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) else: - await self._dimmer.async_on() + await self.device.async_on() self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - await self._dimmer.async_off() + await self.device.async_off() self._attr_is_on = False self.async_write_ha_state() @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_on = self._dimmer.on is True + self._attr_is_on = self.device.on is True - if self._dimmer.brightness is not None: + if (brightness := self.device.brightness) is not None: # Netatmo uses a range of [0, 100] to control brightness - self._attr_brightness = round((self._dimmer.brightness / 100) * 255) + self._attr_brightness = round((brightness / 100) * 255) else: self._attr_brightness = None diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 6680242f579..3fe098a75a9 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -4,11 +4,10 @@ from __future__ import annotations import logging -from pyatmo import DeviceType - from homeassistant.components.select import SelectEntity 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 from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,6 +16,7 @@ from .const import ( DATA_SCHEDULES, DOMAIN, EVENT_TYPE_SCHEDULE, + MANUFACTURER, NETATMO_CREATE_SELECT, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoHome @@ -43,39 +43,36 @@ async def async_setup_entry( class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): """Representation a Netatmo thermostat schedule selector.""" - def __init__( - self, - netatmo_home: NetatmoHome, - ) -> None: + _attr_name = None + + def __init__(self, netatmo_home: NetatmoHome) -> None: """Initialize the select entity.""" - SelectEntity.__init__(self) super().__init__(netatmo_home.data_handler) - self._home = netatmo_home.home - self._home_id = self._home.entity_id + self.home = netatmo_home.home - self._signal_name = netatmo_home.signal_name self._publishers.extend( [ { "name": HOME, - "home_id": self._home.entity_id, - SIGNAL_NAME: self._signal_name, + "home_id": self.home.entity_id, + SIGNAL_NAME: netatmo_home.signal_name, }, ] ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.home.entity_id)}, + name=self.home.name, + manufacturer=MANUFACTURER, + model="Climate", + configuration_url=CONF_URL_ENERGY, + ) - self._device_name = self._home.name - self._attr_name = f"{self._device_name}" + self._attr_unique_id = f"{self.home.entity_id}-schedule-select" - self._model = DeviceType.NATherm1 - self._config_url = CONF_URL_ENERGY - - self._attr_unique_id = f"{self._home_id}-schedule-select" - - self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") + self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") self._attr_options = [ - schedule.name for schedule in self._home.schedules.values() + schedule.name for schedule in self.home.schedules.values() ] async def async_added_to_hass(self) -> None: @@ -95,12 +92,12 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): """Handle webhook events.""" data = event["data"] - if self._home_id != data["home_id"]: + if self.home.entity_id != data["home_id"]: return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: self._attr_current_option = getattr( - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].get( + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( data["schedule_id"] ), "name", @@ -110,24 +107,26 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id + self.home.entity_id ].items(): if schedule.name != option: continue _LOGGER.debug( "Setting %s schedule to %s (%s)", - self._home_id, + self.home.entity_id, option, sid, ) - await self._home.async_switch_schedule(schedule_id=sid) + await self.home.async_switch_schedule(schedule_id=sid) break @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules + self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id] = ( + self.home.schedules + ) self._attr_options = [ - schedule.name for schedule in self._home.schedules.values() + schedule.name for schedule in self.home.schedules.values() ] diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 481b0ba86aa..7d99ef9d32c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -2,11 +2,13 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging -from typing import cast +from typing import Any, cast import pyatmo +from pyatmo.modules import PublicWeatherArea from homeassistant.components.sensor import ( SensorDeviceClass, @@ -31,17 +33,20 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers.device_registry import ( + DeviceInfo, + async_entries_for_config_entry, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import ( CONF_URL_ENERGY, CONF_URL_PUBLIC_WEATHER, - CONF_URL_WEATHER, CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, @@ -52,23 +57,64 @@ from .const import ( SIGNAL_NAME, ) from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom -from .entity import NetatmoBaseEntity +from .entity import ( + NetatmoBaseEntity, + NetatmoModuleEntity, + NetatmoRoomEntity, + NetatmoWeatherModuleEntity, +) from .helper import NetatmoArea _LOGGER = logging.getLogger(__name__) -SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( - "temperature", - "pressure", - "humidity", - "rain", - "wind_strength", - "gust_strength", - "sum_rain_1", - "sum_rain_24", - "wind_angle", - "gust_angle", -) +DIRECTION_OPTIONS = [ + "n", + "ne", + "e", + "se", + "s", + "sw", + "w", + "nw", +] + + +def process_health(health: StateType) -> str | None: + """Process health index and return string for display.""" + if not isinstance(health, int): + return None + return { + 0: "healthy", + 1: "fine", + 2: "fair", + 3: "poor", + }.get(health, "unhealthy") + + +def process_rf(strength: StateType) -> str | None: + """Process wifi signal strength and return string for display.""" + if not isinstance(strength, int): + return None + if strength >= 90: + return "Low" + if strength >= 76: + return "Medium" + if strength >= 60: + return "High" + return "Full" + + +def process_wifi(strength: StateType) -> str | None: + """Process wifi signal strength and return string for display.""" + if not isinstance(strength, int): + return None + if strength >= 86: + return "Low" + if strength >= 71: + return "Medium" + if strength >= 56: + return "High" + return "Full" @dataclass(frozen=True, kw_only=True) @@ -76,27 +122,25 @@ class NetatmoSensorEntityDescription(SensorEntityDescription): """Describes Netatmo sensor entity.""" netatmo_name: str + value_fn: Callable[[StateType], StateType] = lambda x: x SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temperature", - name="Temperature", netatmo_name="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, ), NetatmoSensorEntityDescription( key="temp_trend", - name="Temperature trend", netatmo_name="temp_trend", entity_registry_enabled_default=False, - icon="mdi:trending-up", ), NetatmoSensorEntityDescription( key="co2", - name="CO2", netatmo_name="co2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -104,22 +148,19 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="pressure", - name="Pressure", netatmo_name="pressure", native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + suggested_display_precision=1, ), NetatmoSensorEntityDescription( key="pressure_trend", - name="Pressure trend", netatmo_name="pressure_trend", entity_registry_enabled_default=False, - icon="mdi:trending-up", ), NetatmoSensorEntityDescription( key="noise", - name="Noise", netatmo_name="noise", native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, device_class=SensorDeviceClass.SOUND_PRESSURE, @@ -127,7 +168,6 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="humidity", - name="Humidity", netatmo_name="humidity", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -135,7 +175,6 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="rain", - name="Rain", netatmo_name="rain", native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, @@ -143,16 +182,15 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="sum_rain_1", - name="Rain last hour", netatmo_name="sum_rain_1", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, ), NetatmoSensorEntityDescription( key="sum_rain_24", - name="Rain today", netatmo_name="sum_rain_24", native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, @@ -160,7 +198,6 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="battery_percent", - name="Battery Percent", netatmo_name="battery", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -169,22 +206,20 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="windangle", - name="Direction", netatmo_name="wind_direction", - icon="mdi:compass-outline", + device_class=SensorDeviceClass.ENUM, + options=DIRECTION_OPTIONS, + value_fn=lambda x: x.lower() if isinstance(x, str) else None, ), NetatmoSensorEntityDescription( key="windangle_value", - name="Angle", netatmo_name="wind_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, - icon="mdi:compass-outline", state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="windstrength", - name="Wind Strength", netatmo_name="wind_strength", native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, @@ -192,23 +227,21 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="gustangle", - name="Gust Direction", netatmo_name="gust_direction", entity_registry_enabled_default=False, - icon="mdi:compass-outline", + device_class=SensorDeviceClass.ENUM, + options=DIRECTION_OPTIONS, + value_fn=lambda x: x.lower() if isinstance(x, str) else None, ), NetatmoSensorEntityDescription( key="gustangle_value", - name="Gust Angle", netatmo_name="gust_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, - icon="mdi:compass-outline", state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="guststrength", - name="Gust Strength", netatmo_name="gust_strength", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -217,37 +250,33 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="reachable", - name="Reachability", netatmo_name="reachable", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:signal", ), NetatmoSensorEntityDescription( key="rf_status", - name="Radio", netatmo_name="rf_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:signal", + value_fn=process_rf, ), NetatmoSensorEntityDescription( key="wifi_status", - name="Wifi", netatmo_name="wifi_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:wifi", + value_fn=process_wifi, ), NetatmoSensorEntityDescription( key="health_idx", - name="Health", netatmo_name="health_idx", - icon="mdi:cloud", + device_class=SensorDeviceClass.ENUM, + options=["healthy", "fine", "fair", "poor", "unhealthy"], + value_fn=process_health, ), NetatmoSensorEntityDescription( key="power", - name="Power", netatmo_name="power", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, @@ -256,9 +285,100 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ) SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES] + +@dataclass(frozen=True, kw_only=True) +class NetatmoPublicWeatherSensorEntityDescription(SensorEntityDescription): + """Describes Netatmo sensor entity.""" + + value_fn: Callable[[PublicWeatherArea], dict[str, Any]] + + +PUBLIC_WEATHER_STATION_TYPES: tuple[ + NetatmoPublicWeatherSensorEntityDescription, ... +] = ( + NetatmoPublicWeatherSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + value_fn=lambda area: area.get_latest_temperatures(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="pressure", + native_unit_of_measurement=UnitOfPressure.MBAR, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + suggested_display_precision=1, + value_fn=lambda area: area.get_latest_pressures(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="humidity", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + value_fn=lambda area: area.get_latest_humidities(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_rain(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="sum_rain_1", + translation_key="sum_rain_1", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, + value_fn=lambda area: area.get_60_min_rain(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="sum_rain_24", + translation_key="sum_rain_24", + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda area: area.get_24_h_rain(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="windangle_value", + entity_registry_enabled_default=False, + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_wind_angles(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="windstrength", + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_wind_strengths(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="gustangle_value", + translation_key="gust_angle", + entity_registry_enabled_default=False, + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_gust_angles(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="guststrength", + translation_key="gust_strength", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_gust_strengths(), + ), +) + BATTERY_SENSOR_DESCRIPTION = NetatmoSensorEntityDescription( key="battery", - name="Battery Percent", netatmo_name="battery", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -305,11 +425,9 @@ async def async_setup_entry( netatmo_device.device.name, ) async_add_entities( - [ - NetatmoSensor(netatmo_device, description) - for description in SENSOR_TYPES - if description.key in netatmo_device.device.features - ] + NetatmoSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.key in netatmo_device.device.features ) entry.async_on_unload( @@ -347,7 +465,7 @@ async def async_setup_entry( if device.model == "Public Weather station" } - new_entities = [] + new_entities: list[NetatmoPublicSensor] = [] for area in [ NetatmoArea(**i) for i in entry.options.get(CONF_WEATHER_AREAS, {}).values() ]: @@ -376,11 +494,8 @@ async def async_setup_entry( ) new_entities.extend( - [ - NetatmoPublicSensor(data_handler, area, description) - for description in SENSOR_TYPES - if description.netatmo_name in SUPPORTED_PUBLIC_SENSOR_TYPES - ] + NetatmoPublicSensor(data_handler, area, description) + for description in PUBLIC_WEATHER_STATION_TYPES ) for device_id in entities.values(): @@ -395,10 +510,9 @@ async def async_setup_entry( await add_public_entities(False) -class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): +class NetatmoWeatherSensor(NetatmoWeatherModuleEntity, SensorEntity): """Implementation of a Netatmo weather/home coach sensor.""" - _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription def __init__( @@ -407,89 +521,43 @@ class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) self.entity_description = description + self._attr_translation_key = description.netatmo_name + self._attr_unique_id = f"{self.device.entity_id}-{description.key}" - self._module = netatmo_device.device - self._id = self._module.entity_id - self._station_id = ( - self._module.bridge if self._module.bridge is not None else self._id + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + self.device.reachable + or getattr(self.device, self.entity_description.netatmo_name) is not None ) - self._device_name = self._module.name - category = getattr(self._module.device_category, "name") - self._publishers.extend( - [ - { - "name": category, - SIGNAL_NAME: category, - }, - ] - ) - - self._attr_name = f"{description.name}" - self._model = self._module.device_type - self._config_url = CONF_URL_WEATHER - self._attr_unique_id = f"{self._id}-{description.key}" - - if hasattr(self._module, "place"): - place = cast( - pyatmo.modules.base_class.Place, getattr(self._module, "place") - ) - if hasattr(place, "location") and place.location is not None: - self._attr_extra_state_attributes.update( - { - ATTR_LATITUDE: place.location.latitude, - ATTR_LONGITUDE: place.location.longitude, - } - ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if ( - not self._module.reachable - or (state := getattr(self._module, self.entity_description.netatmo_name)) - is None - ): - if self.available: - self._attr_available = False - return - - if self.entity_description.netatmo_name in { - "temperature", - "pressure", - "sum_rain_1", - }: - self._attr_native_value = round(state, 1) - elif self.entity_description.netatmo_name == "rf_strength": - self._attr_native_value = process_rf(state) - elif self.entity_description.netatmo_name == "wifi_strength": - self._attr_native_value = process_wifi(state) - elif self.entity_description.netatmo_name == "health_idx": - self._attr_native_value = process_health(state) - else: - self._attr_native_value = state - - self._attr_available = True + value = cast( + StateType, getattr(self.device, self.entity_description.netatmo_name) + ) + if value is not None: + value = self.entity_description.value_fn(value) + self._attr_native_value = value self.async_write_ha_state() -class NetatmoClimateBatterySensor(NetatmoBaseEntity, SensorEntity): +class NetatmoClimateBatterySensor(NetatmoModuleEntity, SensorEntity): """Implementation of a Netatmo sensor.""" entity_description: NetatmoSensorEntityDescription + device: pyatmo.modules.NRV + _attr_configuration_url = CONF_URL_ENERGY - def __init__( - self, - netatmo_device: NetatmoDevice, - ) -> None: + def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize the sensor.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) self.entity_description = BATTERY_SENSOR_DESCRIPTION - self._module = cast(pyatmo.modules.NRV, netatmo_device.device) - self._id = netatmo_device.parent_id - self._publishers.extend( [ { @@ -500,31 +568,32 @@ class NetatmoClimateBatterySensor(NetatmoBaseEntity, SensorEntity): ] ) - self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._room_id = self._module.room_id - self._model = getattr(self._module.device_type, "value") - self._config_url = CONF_URL_ENERGY - - self._attr_unique_id = ( - f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + self._attr_unique_id = f"{netatmo_device.parent_id}-{self.device.entity_id}-{self.entity_description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, netatmo_device.parent_id)}, + name=netatmo_device.device.name, + manufacturer=self.device_description[0], + model=self.device_description[1], + configuration_url=self._attr_configuration_url, ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if not self._module.reachable: + if not self.device.reachable: if self.available: self._attr_available = False return self._attr_available = True - self._attr_native_value = self._module.battery + self._attr_native_value = self.device.battery -class NetatmoSensor(NetatmoBaseEntity, SensorEntity): +class NetatmoSensor(NetatmoModuleEntity, SensorEntity): """Implementation of a Netatmo sensor.""" entity_description: NetatmoSensorEntityDescription + _attr_configuration_url = CONF_URL_ENERGY def __init__( self, @@ -532,40 +601,32 @@ class NetatmoSensor(NetatmoBaseEntity, SensorEntity): description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) self.entity_description = description - self._module = netatmo_device.device - self._id = self._module.entity_id - self._publishers.extend( [ { "name": HOME, - "home_id": netatmo_device.device.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: netatmo_device.signal_name, }, ] ) - self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._room_id = self._module.room_id - self._model = getattr(self._module.device_type, "value") - self._config_url = CONF_URL_ENERGY - self._attr_unique_id = ( - f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + f"{self.device.entity_id}-{self.device.entity_id}-{description.key}" ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if not self._module.reachable: + if not self.device.reachable: if self.available: self._attr_available = False return - if (state := getattr(self._module, self.entity_description.key)) is None: + if (state := getattr(self.device, self.entity_description.key)) is None: return self._attr_available = True @@ -574,42 +635,7 @@ class NetatmoSensor(NetatmoBaseEntity, SensorEntity): self.async_write_ha_state() -def process_health(health: int) -> str: - """Process health index and return string for display.""" - if health == 0: - return "Healthy" - if health == 1: - return "Fine" - if health == 2: - return "Fair" - if health == 3: - return "Poor" - return "Unhealthy" - - -def process_rf(strength: int) -> str: - """Process wifi signal strength and return string for display.""" - if strength >= 90: - return "Low" - if strength >= 76: - return "Medium" - if strength >= 60: - return "High" - return "Full" - - -def process_wifi(strength: int) -> str: - """Process wifi signal strength and return string for display.""" - if strength >= 86: - return "Low" - if strength >= 71: - return "Medium" - if strength >= 56: - return "High" - return "Full" - - -class NetatmoRoomSensor(NetatmoBaseEntity, SensorEntity): +class NetatmoRoomSensor(NetatmoRoomEntity, SensorEntity): """Implementation of a Netatmo room sensor.""" entity_description: NetatmoSensorEntityDescription @@ -620,37 +646,27 @@ class NetatmoRoomSensor(NetatmoBaseEntity, SensorEntity): description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(netatmo_room.data_handler) + super().__init__(netatmo_room) self.entity_description = description - self._room = netatmo_room.room - self._id = self._room.entity_id - self._publishers.extend( [ { "name": HOME, - "home_id": netatmo_room.room.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: netatmo_room.signal_name, }, ] ) - self._attr_name = f"{self._room.name} {self.entity_description.name}" - self._room_id = self._room.entity_id - self._config_url = CONF_URL_ENERGY - - assert self._room.climate_type - self._model = self._room.climate_type - self._attr_unique_id = ( - f"{self._id}-{self._room.entity_id}-{self.entity_description.key}" + f"{self.device.entity_id}-{self.device.entity_id}-{description.key}" ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if (state := getattr(self._room, self.entity_description.key)) is None: + if (state := getattr(self.device, self.entity_description.key)) is None: return self._attr_native_value = state @@ -661,14 +677,13 @@ class NetatmoRoomSensor(NetatmoBaseEntity, SensorEntity): class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): """Represent a single sensor in a Netatmo.""" - _attr_has_entity_name = True - entity_description: NetatmoSensorEntityDescription + entity_description: NetatmoPublicWeatherSensorEntityDescription def __init__( self, data_handler: NetatmoDataHandler, area: NetatmoArea, - description: NetatmoSensorEntityDescription, + description: NetatmoPublicWeatherSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data_handler) @@ -691,33 +706,31 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): self.area = area self._mode = area.mode - self._area_name = area.area_name - self._id = self._area_name - self._device_name = f"{self._area_name}" - self._attr_name = f"{description.name}" self._show_on_map = area.show_on_map - self._config_url = CONF_URL_PUBLIC_WEATHER - self._attr_unique_id = ( - f"{self._device_name.replace(' ', '-')}-{description.key}" - ) - self._model = PUBLIC + self._attr_unique_id = f"{area.area_name.replace(' ', '-')}-{description.key}" self._attr_extra_state_attributes.update( { - ATTR_LATITUDE: (self.area.lat_ne + self.area.lat_sw) / 2, - ATTR_LONGITUDE: (self.area.lon_ne + self.area.lon_sw) / 2, + ATTR_LATITUDE: (area.lat_ne + area.lat_sw) / 2, + ATTR_LONGITUDE: (area.lon_ne + area.lon_sw) / 2, } ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, area.area_name)}, + name=area.area_name, + model="Public Weather station", + manufacturer="Netatmo", + configuration_url=CONF_URL_PUBLIC_WEATHER, + ) async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - assert self.device_info and "name" in self.device_info self.async_on_remove( async_dispatcher_connect( self.hass, - f"netatmo-config-{self.device_info['name']}", + f"netatmo-config-{self.area.area_name}", self.async_config_update_callback, ) ) @@ -748,35 +761,14 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - data = None - - if self.entity_description.netatmo_name == "temperature": - data = self._station.get_latest_temperatures() - elif self.entity_description.netatmo_name == "pressure": - data = self._station.get_latest_pressures() - elif self.entity_description.netatmo_name == "humidity": - data = self._station.get_latest_humidities() - elif self.entity_description.netatmo_name == "rain": - data = self._station.get_latest_rain() - elif self.entity_description.netatmo_name == "sum_rain_1": - data = self._station.get_60_min_rain() - elif self.entity_description.netatmo_name == "sum_rain_24": - data = self._station.get_24_h_rain() - elif self.entity_description.netatmo_name == "wind_strength": - data = self._station.get_latest_wind_strengths() - elif self.entity_description.netatmo_name == "gust_strength": - data = self._station.get_latest_gust_strengths() - elif self.entity_description.netatmo_name == "wind_angle": - data = self._station.get_latest_wind_angles() - elif self.entity_description.netatmo_name == "gust_angle": - data = self._station.get_latest_gust_angles() + data = self.entity_description.value_fn(self._station) if not data: if self.available: _LOGGER.error( "No station provides %s data in the area %s", self.entity_description.key, - self._area_name, + self.area.area_name, ) self._attr_available = False @@ -788,5 +780,5 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): elif self._mode == "max": self._attr_native_value = max(values) - self._attr_available = self.state is not None + self._attr_available = self.native_value is not None self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index e504b27b599..3c360634147 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -166,5 +166,78 @@ "name": "Clear temperature setting", "description": "Clears any temperature setting for a Netatmo climate device reverting it to the current preset or schedule." } + }, + "entity": { + "sensor": { + "temp_trend": { + "name": "Temperature trend" + }, + "pressure_trend": { + "name": "Pressure trend" + }, + "noise": { + "name": "Noise" + }, + "sum_rain_1": { + "name": "Precipitation last hour" + }, + "sum_rain_24": { + "name": "Precipitation today" + }, + "wind_direction": { + "name": "Wind direction", + "state": { + "n": "North", + "ne": "North-east", + "e": "East", + "se": "South-east", + "s": "South", + "sw": "South-west", + "w": "West", + "nw": "North-west" + } + }, + "wind_angle": { + "name": "Wind angle" + }, + "gust_direction": { + "name": "Gust direction", + "state": { + "n": "[%key:component::netatmo::entity::sensor::wind_direction::state::n%]", + "ne": "[%key:component::netatmo::entity::sensor::wind_direction::state::ne%]", + "e": "[%key:component::netatmo::entity::sensor::wind_direction::state::e%]", + "se": "[%key:component::netatmo::entity::sensor::wind_direction::state::se%]", + "s": "[%key:component::netatmo::entity::sensor::wind_direction::state::s%]", + "sw": "[%key:component::netatmo::entity::sensor::wind_direction::state::sw%]", + "w": "[%key:component::netatmo::entity::sensor::wind_direction::state::w%]", + "nw": "[%key:component::netatmo::entity::sensor::wind_direction::state::nw%]" + } + }, + "gust_angle": { + "name": "Gust angle" + }, + "gust_strength": { + "name": "Gust strength" + }, + "reachable": { + "name": "Reachability" + }, + "rf_strength": { + "name": "Radio" + }, + "wifi_strength": { + "name": "Wi-Fi" + }, + "health_idx": { + "name": "Health index", + "state": { + "healthy": "Healthy", + "fine": "Fine", + "fair": "Fair", + "poor": "Poor", + "unhealthy": "Unhealthy" + } + } + } } } diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py index 6677adec4b0..6ba4628a358 100644 --- a/homeassistant/components/netatmo/switch.py +++ b/homeassistant/components/netatmo/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from pyatmo import modules as NaModules @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_URL_CONTROL, NETATMO_CREATE_SWITCH from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -38,51 +38,45 @@ async def async_setup_entry( ) -class NetatmoSwitch(NetatmoBaseEntity, SwitchEntity): +class NetatmoSwitch(NetatmoModuleEntity, SwitchEntity): """Representation of a Netatmo switch device.""" + _attr_name = None + _attr_configuration_url = CONF_URL_CONTROL + device: NaModules.Switch + def __init__( self, netatmo_device: NetatmoDevice, ) -> None: """Initialize the Netatmo device.""" - super().__init__(netatmo_device.data_handler) - - self._switch = cast(NaModules.Switch, netatmo_device.device) - - self._id = self._switch.entity_id - self._attr_name = self._device_name = self._switch.name - self._model = self._switch.device_type - self._config_url = CONF_URL_CONTROL - - self._home_id = self._switch.home.entity_id - - self._signal_name = f"{HOME}-{self._home_id}" + super().__init__(netatmo_device) + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - self._attr_unique_id = f"{self._id}-{self._model}" - self._attr_is_on = self._switch.on + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" + self._attr_is_on = self.device.on @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_on = self._switch.on + self._attr_is_on = self.device.on async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" - await self._switch.async_on() + await self.device.async_on() self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" - await self._switch.async_off() + await self.device.async_off() self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/nextbus/config_flow.py b/homeassistant/components/nextbus/config_flow.py index 1d2b7f0b00f..c7e5ed3f36f 100644 --- a/homeassistant/components/nextbus/config_flow.py +++ b/homeassistant/components/nextbus/config_flow.py @@ -7,7 +7,7 @@ from py_nextbus import NextBusClient import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_NAME, CONF_STOP +from homeassistant.const import CONF_STOP from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -102,41 +102,6 @@ class NextBusFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize NextBus config flow.""" self.data: dict[str, str] = {} self._client = NextBusClient(output_format="json") - _LOGGER.info("Init new config flow") - - 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] - stop_tag = config_input[CONF_STOP] - - validation_result = await self.hass.async_add_executor_job( - _validate_import, - self._client, - agency_tag, - route_tag, - stop_tag, - ) - if isinstance(validation_result, str): - return self.async_abort(reason=validation_result) - - data = { - CONF_AGENCY: agency_tag, - CONF_ROUTE: route_tag, - CONF_STOP: stop_tag, - CONF_NAME: config_input.get( - CONF_NAME, - f"{config_input[CONF_AGENCY]} {config_input[CONF_ROUTE]}", - ), - } - - await self.async_set_unique_id(_unique_id_from_data(data)) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=" ".join(validation_result), - data=data, - ) async def async_step_user( self, diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index abf280bece9..15377bce56b 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -72,8 +72,8 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): # Casting here because we expect dict and not a str due to the input format selected being JSON data = cast(dict[str, Any], data) self._calc_predictions(data) - return data except (NextBusHTTPError, NextBusFormatError) as ex: raise UpdateFailed("Failed updating nextbus data", ex) from ex + return data return await self.hass.async_add_executor_job(_update_data) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 68d10726609..8cd0d177835 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -6,20 +6,11 @@ from itertools import chain import logging from typing import cast -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_STOP -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback 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 homeassistant.util.dt import utc_from_timestamp @@ -29,43 +20,6 @@ from .util import listify, maybe_first _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_AGENCY): cv.string, - vol.Required(CONF_ROUTE): cv.string, - vol.Required(CONF_STOP): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Initialize nextbus import from config.""" - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - is_fixable=False, - breaks_in_ha_version="2024.4.0", - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "NextBus", - }, - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, @@ -128,11 +82,13 @@ class NextBusDepartureSensor( def _log_debug(self, message, *args): """Log debug message with prefix.""" - _LOGGER.debug(":".join((self.agency, self.route, self.stop, message)), *args) + msg = f"{self.agency}:{self.route}:{self.stop}:{message}" + _LOGGER.debug(msg, *args) def _log_err(self, message, *args): """Log error message with prefix.""" - _LOGGER.error(":".join((self.agency, self.route, self.stop, message)), *args) + msg = f"{self.agency}:{self.route}:{self.stop}:{message}" + _LOGGER.error(msg, *args) async def async_added_to_hass(self) -> None: """Read data from coordinator after adding to hass.""" diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 389173a2694..c7e4a0842fb 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -4,31 +4,15 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging -from typing import TypeVar from aiohttp.client_exceptions import ClientConnectorError -from nextdns import ( - AnalyticsDnssec, - AnalyticsEncryption, - AnalyticsIpVersions, - AnalyticsProtocols, - AnalyticsStatus, - ApiError, - ConnectionStatus, - InvalidApiKeyError, - NextDns, - Settings, -) -from nextdns.model import NextDnsData +from nextdns import ApiError, NextDns from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_CONNECTION, @@ -44,104 +28,16 @@ from .const import ( UPDATE_INTERVAL_CONNECTION, UPDATE_INTERVAL_SETTINGS, ) - -CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData) - - -class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS data API.""" - - def __init__( - self, - hass: HomeAssistant, - nextdns: NextDns, - profile_id: str, - update_interval: timedelta, - ) -> None: - """Initialize.""" - self.nextdns = nextdns - self.profile_id = profile_id - self.profile_name = nextdns.get_profile_name(profile_id) - self.device_info = DeviceInfo( - configuration_url=f"https://my.nextdns.io/{profile_id}/setup", - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, str(profile_id))}, - manufacturer="NextDNS Inc.", - name=self.profile_name, - ) - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - - async def _async_update_data(self) -> CoordinatorDataT: - """Update data via internal method.""" - try: - async with asyncio.timeout(10): - return await self._async_update_data_internal() - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err - - async def _async_update_data_internal(self) -> CoordinatorDataT: - """Update data via library.""" - raise NotImplementedError("Update method not implemented") - - -class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics status data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsStatus: - """Update data via library.""" - return await self.nextdns.get_analytics_status(self.profile_id) - - -class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics Dnssec data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsDnssec: - """Update data via library.""" - return await self.nextdns.get_analytics_dnssec(self.profile_id) - - -class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics encryption data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsEncryption: - """Update data via library.""" - return await self.nextdns.get_analytics_encryption(self.profile_id) - - -class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics IP versions data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsIpVersions: - """Update data via library.""" - return await self.nextdns.get_analytics_ip_versions(self.profile_id) - - -class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics protocols data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsProtocols: - """Update data via library.""" - return await self.nextdns.get_analytics_protocols(self.profile_id) - - -class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS connection data from API.""" - - async def _async_update_data_internal(self) -> Settings: - """Update data via library.""" - return await self.nextdns.get_settings(self.profile_id) - - -class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStatus]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS connection data from API.""" - - async def _async_update_data_internal(self) -> ConnectionStatus: - """Update data via library.""" - return await self.nextdns.connection_status(self.profile_id) - - -_LOGGER = logging.getLogger(__name__) +from .coordinator import ( + NextDnsConnectionUpdateCoordinator, + NextDnsDnssecUpdateCoordinator, + NextDnsEncryptionUpdateCoordinator, + NextDnsIpVersionsUpdateCoordinator, + NextDnsProtocolsUpdateCoordinator, + NextDnsSettingsUpdateCoordinator, + NextDnsStatusUpdateCoordinator, + NextDnsUpdateCoordinator, +) PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [ diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py index f6860586808..1bb79cf4fce 100644 --- a/homeassistant/components/nextdns/binary_sensor.py +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -19,8 +19,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CoordinatorDataT, NextDnsConnectionUpdateCoordinator from .const import ATTR_CONNECTION, DOMAIN +from .coordinator import CoordinatorDataT, NextDnsConnectionUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py index d74152248a5..d61c953f260 100644 --- a/homeassistant/components/nextdns/button.py +++ b/homeassistant/components/nextdns/button.py @@ -9,8 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsStatusUpdateCoordinator from .const import ATTR_STATUS, DOMAIN +from .coordinator import NextDnsStatusUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py new file mode 100644 index 00000000000..cad1aeac070 --- /dev/null +++ b/homeassistant/components/nextdns/coordinator.py @@ -0,0 +1,124 @@ +"""NextDns coordinator.""" + +import asyncio +from datetime import timedelta +import logging +from typing import TypeVar + +from aiohttp.client_exceptions import ClientConnectorError +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + ApiError, + ConnectionStatus, + InvalidApiKeyError, + NextDns, + Settings, +) +from nextdns.model import NextDnsData + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData) + + +class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): + """Class to manage fetching NextDNS data API.""" + + def __init__( + self, + hass: HomeAssistant, + nextdns: NextDns, + profile_id: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.nextdns = nextdns + self.profile_id = profile_id + self.profile_name = nextdns.get_profile_name(profile_id) + self.device_info = DeviceInfo( + configuration_url=f"https://my.nextdns.io/{profile_id}/setup", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(profile_id))}, + manufacturer="NextDNS Inc.", + name=self.profile_name, + ) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> CoordinatorDataT: + """Update data via internal method.""" + try: + async with asyncio.timeout(10): + return await self._async_update_data_internal() + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + async def _async_update_data_internal(self) -> CoordinatorDataT: + """Update data via library.""" + raise NotImplementedError("Update method not implemented") + + +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): + """Class to manage fetching NextDNS analytics status data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsStatus: + """Update data via library.""" + return await self.nextdns.get_analytics_status(self.profile_id) + + +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): + """Class to manage fetching NextDNS analytics Dnssec data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsDnssec: + """Update data via library.""" + return await self.nextdns.get_analytics_dnssec(self.profile_id) + + +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): + """Class to manage fetching NextDNS analytics encryption data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsEncryption: + """Update data via library.""" + return await self.nextdns.get_analytics_encryption(self.profile_id) + + +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): + """Class to manage fetching NextDNS analytics IP versions data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsIpVersions: + """Update data via library.""" + return await self.nextdns.get_analytics_ip_versions(self.profile_id) + + +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): + """Class to manage fetching NextDNS analytics protocols data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsProtocols: + """Update data via library.""" + return await self.nextdns.get_analytics_protocols(self.profile_id) + + +class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> Settings: + """Update data via library.""" + return await self.nextdns.get_settings(self.profile_id) + + +class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStatus]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> ConnectionStatus: + """Update data via library.""" + return await self.nextdns.connection_status(self.profile_id) diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index c0a9071bb9d..cade6476d82 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -37,7 +37,7 @@ async def async_get_config_entry_diagnostics( settings_coordinator = coordinators[ATTR_SETTINGS] status_coordinator = coordinators[ATTR_STATUS] - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "dnssec_coordinator_data": asdict(dnssec_coordinator.data), "encryption_coordinator_data": asdict(encryption_coordinator.data), @@ -46,5 +46,3 @@ async def async_get_config_entry_diagnostics( "settings_coordinator_data": asdict(settings_coordinator.data), "status_coordinator_data": asdict(status_coordinator.data), } - - return diagnostics_data diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 611021d73e4..1e7145ef6d1 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["nextdns"], "quality_scale": "platinum", - "requirements": ["nextdns==2.1.0"] + "requirements": ["nextdns==3.0.0"] } diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 4357179cbdb..3ac2179ed31 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -26,7 +26,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CoordinatorDataT, NextDnsUpdateCoordinator from .const import ( ATTR_DNSSEC, ATTR_ENCRYPTION, @@ -35,6 +34,7 @@ from .const import ( ATTR_STATUS, DOMAIN, ) +from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index 81bf8b4e8c6..dfb796efd8c 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -18,8 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CoordinatorDataT, NextDnsSettingsUpdateCoordinator from .const import ATTR_SETTINGS, DOMAIN +from .coordinator import CoordinatorDataT, NextDnsSettingsUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index dd42a0ab10b..dd6b15400d9 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -19,6 +19,7 @@ from homeassistant.components.notify import ( ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,6 +45,7 @@ from .const import ( ATTR_POSITION, ATTR_TRANSPARENCY, DEFAULT_TIMEOUT, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) @@ -133,21 +135,49 @@ class NFAndroidTVNotificationService(BaseNotificationService): "Invalid interrupt-value: %s", data.get(ATTR_INTERRUPT) ) if imagedata := data.get(ATTR_IMAGE): - image_file = self.load_file( - url=imagedata.get(ATTR_IMAGE_URL), - local_path=imagedata.get(ATTR_IMAGE_PATH), - username=imagedata.get(ATTR_IMAGE_USERNAME), - password=imagedata.get(ATTR_IMAGE_PASSWORD), - auth=imagedata.get(ATTR_IMAGE_AUTH), - ) + if isinstance(imagedata, str): + image_file = ( + self.load_file(url=imagedata) + if imagedata.startswith("http") + else self.load_file(local_path=imagedata) + ) + elif isinstance(imagedata, dict): + image_file = self.load_file( + url=imagedata.get(ATTR_IMAGE_URL), + local_path=imagedata.get(ATTR_IMAGE_PATH), + username=imagedata.get(ATTR_IMAGE_USERNAME), + password=imagedata.get(ATTR_IMAGE_PASSWORD), + auth=imagedata.get(ATTR_IMAGE_AUTH), + ) + else: + raise ServiceValidationError( + "Invalid image provided", + translation_domain=DOMAIN, + translation_key="invalid_notification_image", + translation_placeholders={"type": type(imagedata).__name__}, + ) if icondata := data.get(ATTR_ICON): - icon = self.load_file( - url=icondata.get(ATTR_ICON_URL), - local_path=icondata.get(ATTR_ICON_PATH), - username=icondata.get(ATTR_ICON_USERNAME), - password=icondata.get(ATTR_ICON_PASSWORD), - auth=icondata.get(ATTR_ICON_AUTH), - ) + if isinstance(icondata, str): + icondata = ( + self.load_file(url=icondata) + if icondata.startswith("http") + else self.load_file(local_path=icondata) + ) + elif isinstance(icondata, dict): + icon = self.load_file( + url=icondata.get(ATTR_ICON_URL), + local_path=icondata.get(ATTR_ICON_PATH), + username=icondata.get(ATTR_ICON_USERNAME), + password=icondata.get(ATTR_ICON_PASSWORD), + auth=icondata.get(ATTR_ICON_AUTH), + ) + else: + raise ServiceValidationError( + "Invalid Icon provided", + translation_domain=DOMAIN, + translation_key="invalid_notification_icon", + translation_placeholders={"type": type(icondata).__name__}, + ) self.notify.send( message, title=title, diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json index cde02327712..e73fc68d66a 100644 --- a/homeassistant/components/nfandroidtv/strings.json +++ b/homeassistant/components/nfandroidtv/strings.json @@ -1,4 +1,12 @@ { + "exceptions": { + "invalid_notification_icon": { + "message": "Invalid icon data provided. Got {type}" + }, + "invalid_notification_image": { + "message": "Invalid image data provided. Got {type}" + } + }, "config": { "step": { "user": { diff --git a/homeassistant/components/nibe_heatpump/coordinator.py b/homeassistant/components/nibe_heatpump/coordinator.py index 1711c3d8f2a..fc212faee71 100644 --- a/homeassistant/components/nibe_heatpump/coordinator.py +++ b/homeassistant/components/nibe_heatpump/coordinator.py @@ -6,6 +6,7 @@ import asyncio from collections import defaultdict from collections.abc import Callable, Iterable from datetime import date, timedelta +from functools import cached_property from typing import Any, Generic, TypeVar from nibe.coil import Coil, CoilData @@ -13,7 +14,6 @@ from nibe.connection import Connection from nibe.exceptions import CoilNotFoundException, ReadException from nibe.heatpump import HeatPump, Series -from homeassistant.backports.functools import cached_property from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -136,7 +136,7 @@ class Coordinator(ContextCoordinator[dict[int, CoilData], int]): return float(value) # type: ignore[arg-type] return None - async def async_write_coil(self, coil: Coil, value: int | float | str) -> None: + async def async_write_coil(self, coil: Coil, value: float | str) -> None: """Write coil and update state.""" data = CoilData(coil, value) await self.connection.write_coil(data) @@ -224,7 +224,7 @@ class CoilEntity(CoordinatorEntity[Coordinator]): def _async_read_coil(self, data: CoilData): """Update state of entity based on coil data.""" - async def _async_write_coil(self, value: int | float | str): + async def _async_write_coil(self, value: float | str): """Write coil and update state.""" await self.coordinator.async_write_coil(self._coil, value) diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index 07c3f6fe9a1..3b8b290d6c8 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -107,9 +107,6 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, Any] = {} - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if not self._all_region_codes_sorted: nina: Nina = Nina(async_get_clientsession(self.hass)) diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 1bf670aedf0..53a54f26dcf 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], - "requirements": ["PyNINA==0.3.3"] + "requirements": ["PyNINA==0.3.3"], + "single_config_entry": true } diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 5e0393d024f..98ea88d8798 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -15,9 +15,6 @@ } } }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "no_selection": "Please select at least one city/county", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 9eaca66119f..2cbec236261 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -418,13 +418,13 @@ class LeafDataStore: server_info = await self.hass.async_add_executor_job( self.leaf.get_latest_battery_status ) - return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status") return None except (KeyError, TypeError): _LOGGER.error("An error occurred parsing response from server") return None + return server_info async def async_get_climate( self, diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index 6128272fbbb..a89c50a2210 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -62,7 +62,7 @@ def _normalize_ips_and_network(hosts_str: str) -> list[str] | None: start, end = host.split("-", 1) if "." not in end: ip_1, ip_2, ip_3, _ = start.split(".", 3) - end = ".".join([ip_1, ip_2, ip_3, end]) + end = f"{ip_1}.{ip_2}.{ip_3}.{end}" summarize_address_range(ip_address(start), ip_address(end)) except ValueError: pass diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 235263345e6..5e213e847ba 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -85,7 +85,7 @@ class NOAATidesData(TypedDict): """Representation of a single tide.""" time_stamp: list[Timestamp] - hi_lo: list[Literal["L"] | Literal["H"]] + hi_lo: list[Literal["L", "H"]] predicted_wl: list[float] diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index e7390a49676..81b7d300acc 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -2,24 +2,36 @@ from __future__ import annotations +from datetime import timedelta +from functools import cached_property, partial +import logging +from typing import Any, final, override + import voluptuous as vol import homeassistant.components.persistent_notification as pn -from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util from .const import ( # noqa: F401 ATTR_DATA, ATTR_MESSAGE, + ATTR_RECIPIENTS, ATTR_TARGET, ATTR_TITLE, DOMAIN, NOTIFY_SERVICE_SCHEMA, SERVICE_NOTIFY, SERVICE_PERSISTENT_NOTIFICATION, + SERVICE_SEND_MESSAGE, ) from .legacy import ( # noqa: F401 BaseNotificationService, @@ -29,9 +41,17 @@ from .legacy import ( # noqa: F401 check_templates_warn, ) +# mypy: disallow-any-generics + # Platform specific data ATTR_TITLE_DEFAULT = "Home Assistant" +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string}, extra=vol.ALLOW_EXTRA, @@ -50,6 +70,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # legacy platforms to finish setting up. hass.async_create_task(setup, eager_start=True) + component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass) + component.async_register_entity_service( + SERVICE_SEND_MESSAGE, + {vol.Required(ATTR_MESSAGE): cv.string}, + "_async_send_message", + ) + async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistent_notify integration.""" message: Template = service.data[ATTR_MESSAGE] @@ -79,3 +106,66 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return True + + +class NotifyEntityDescription(EntityDescription, frozen_or_thawed=True): + """A class that describes button entities.""" + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[NotifyEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[NotifyEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class NotifyEntity(RestoreEntity): + """Representation of a notify entity.""" + + entity_description: NotifyEntityDescription + _attr_should_poll = False + _attr_device_class: None + _attr_state: None = None + __last_notified_isoformat: str | None = None + + @cached_property + @final + @override + def state(self) -> str | None: + """Return the entity state.""" + return self.__last_notified_isoformat + + def __set_state(self, state: str | None) -> None: + """Invalidate the cache of the cached property.""" + self.__dict__.pop("state", None) + self.__last_notified_isoformat = state + + async def async_internal_added_to_hass(self) -> None: + """Call when the notify entity is added to hass.""" + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if state is not None and state.state not in (STATE_UNAVAILABLE, None): + self.__set_state(state.state) + + @final + async def _async_send_message(self, **kwargs: Any) -> None: + """Send a notification message (from e.g., service call). + + Should not be overridden, handle setting last notification timestamp. + """ + self.__set_state(dt_util.utcnow().isoformat()) + self.async_write_ha_state() + await self.async_send_message(**kwargs) + + def send_message(self, message: str) -> None: + """Send a message.""" + raise NotImplementedError + + async def async_send_message(self, message: str) -> None: + """Send a message.""" + await self.hass.async_add_executor_job(partial(self.send_message, message)) diff --git a/homeassistant/components/notify/const.py b/homeassistant/components/notify/const.py index b653b5d1cbf..6cd957e3afe 100644 --- a/homeassistant/components/notify/const.py +++ b/homeassistant/components/notify/const.py @@ -11,9 +11,12 @@ ATTR_DATA = "data" # Text to notify user of ATTR_MESSAGE = "message" -# Target of the notification (user, device, etc) +# Target of the (legacy) notification (user, device, etc) ATTR_TARGET = "target" +# Recipients for a notification +ATTR_RECIPIENTS = "recipients" + # Title of notification ATTR_TITLE = "title" @@ -22,6 +25,7 @@ DOMAIN = "notify" LOGGER = logging.getLogger(__package__) SERVICE_NOTIFY = "notify" +SERVICE_SEND_MESSAGE = "send_message" SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification" NOTIFY_SERVICE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/notify/icons.json b/homeassistant/components/notify/icons.json index 88577bc2356..ace8ee0c96b 100644 --- a/homeassistant/components/notify/icons.json +++ b/homeassistant/components/notify/icons.json @@ -1,6 +1,12 @@ { + "entity_component": { + "_": { + "default": "mdi:message" + } + }, "services": { "notify": "mdi:bell-ring", - "persistent_notification": "mdi:bell-badge" + "persistent_notification": "mdi:bell-badge", + "send_message": "mdi:message-arrow-right" } } diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 8d053e3af58..ae2a0254761 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -20,6 +20,16 @@ notify: selector: object: +send_message: + target: + entity: + domain: notify + fields: + message: + required: true + selector: + text: + persistent_notification: fields: message: diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json index cff7b265c37..b0dca501509 100644 --- a/homeassistant/components/notify/strings.json +++ b/homeassistant/components/notify/strings.json @@ -1,5 +1,10 @@ { "title": "Notifications", + "entity_component": { + "_": { + "name": "[%key:component::notify::title%]" + } + }, "services": { "notify": { "name": "Send a notification", @@ -23,6 +28,16 @@ } } }, + "send_message": { + "name": "Send a notification message", + "description": "Sends a notification message.", + "fields": { + "message": { + "name": "Message", + "description": "Your notification message." + } + } + }, "persistent_notification": { "name": "Send a persistent notification", "description": "Sends a notification that is visible in the **Notifications** panel.", diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index f2a98599e27..9b4772ee108 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -55,10 +55,9 @@ class NukiDoorsensorEntity(NukiEntity[NukiDevice], BinarySensorEntity): @property def extra_state_attributes(self): """Return the device specific state attributes.""" - data = { + return { ATTR_NUKI_ID: self._nuki_device.nuki_id, } - return data @property def available(self) -> bool: @@ -96,10 +95,9 @@ class NukiRingactionEntity(NukiEntity[NukiDevice], BinarySensorEntity): @property def extra_state_attributes(self): """Return the device specific state attributes.""" - data = { + return { ATTR_NUKI_ID: self._nuki_device.nuki_id, } - return data @property def is_on(self) -> bool: diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 6efc3f6160f..ef71e00bc73 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -116,8 +116,7 @@ class NumatoGpioAdc(SensorEntity): def _clamp_to_source_range(self, val): # clamp to source range val = max(val, self._src_range[0]) - val = min(val, self._src_range[1]) - return val + return min(val, self._src_range[1]) def _linear_scale_to_dest_range(self, val): # linear scale to dest range @@ -125,5 +124,4 @@ class NumatoGpioAdc(SensorEntity): adc_val_rel = val - self._src_range[0] ratio = float(adc_val_rel) / float(src_len) dst_len = self._dst_range[1] - self._dst_range[0] - dest_val = self._dst_range[0] + ratio * dst_len - return dest_val + return self._dst_range[0] + ratio * dst_len diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 8c55bbc2cba..e5b307f5e57 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -6,6 +6,7 @@ from collections.abc import Callable from contextlib import suppress import dataclasses from datetime import timedelta +from functools import cached_property import logging from math import ceil, floor from typing import TYPE_CHECKING, Any, Self, final @@ -15,7 +16,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature from homeassistant.core import HomeAssistant, ServiceCall, async_get_hass, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -30,6 +31,7 @@ from .const import ( # noqa: F401 ATTR_MAX, ATTR_MIN, ATTR_STEP, + ATTR_STEP_VALIDATION, ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, @@ -43,11 +45,6 @@ from .const import ( # noqa: F401 ) from .websocket_api import async_setup as async_setup_ws_api -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -99,10 +96,17 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No """Service call wrapper to set a new value.""" value = service_call.data["value"] if value < entity.min_value or value > entity.max_value: - raise ValueError( - f"Value {value} for {entity.entity_id} is outside valid range" - f" {entity.min_value} - {entity.max_value}" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="out_of_range", + translation_placeholders={ + "value": value, + "entity_id": entity.entity_id, + "min_value": str(entity.min_value), + "max_value": str(entity.max_value), + }, ) + try: native_value = entity.convert_to_native_value(value) # Clamp to the native range @@ -174,7 +178,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Representation of a Number entity.""" _entity_component_unrecorded_attributes = frozenset( - {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_MODE} + {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_STEP_VALIDATION, ATTR_MODE} ) entity_description: NumberEntityDescription diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 89829adcc50..f279ffb72a8 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -54,6 +54,7 @@ ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" +ATTR_STEP_VALIDATION = "step_validation" DEFAULT_MIN_VALUE = 0.0 DEFAULT_MAX_VALUE = 100.0 diff --git a/homeassistant/components/number/significant_change.py b/homeassistant/components/number/significant_change.py index ff77f25e527..14cb2246615 100644 --- a/homeassistant/components/number/significant_change.py +++ b/homeassistant/components/number/significant_change.py @@ -21,10 +21,10 @@ from .const import NumberDeviceClass def _absolute_and_relative_change( - old_state: int | float | None, - new_state: int | float | None, - absolute_change: int | float, - percentage_change: int | float, + old_state: float | None, + new_state: float | None, + absolute_change: float, + percentage_change: float, ) -> bool: return check_absolute_change( old_state, new_state, absolute_change diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index ffddc0c2b3c..502b2b4affd 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -161,6 +161,11 @@ "name": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, + "exceptions": { + "out_of_range": { + "message": "Value {value} for {entity_id} is outside valid range {min_value} - {max_value}." + } + }, "services": { "set_value": { "name": "Set", diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 575def8bf0f..8b715237e01 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -265,9 +265,7 @@ class PyNUTData: manufacturer = _manufacturer_from_status(self._status) model = _model_from_status(self._status) firmware = _firmware_from_status(self._status) - device_info = NUTDeviceInfo(manufacturer, model, firmware) - - return device_info + return NUTDeviceInfo(manufacturer, model, firmware) async def _async_get_status(self) -> dict[str, str]: """Get the ups status from NUT.""" diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 91920b4c32d..344767c8cd1 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -109,15 +109,15 @@ class ObihaiServiceSensors(SensorEntity): LOGGER.info("Connection restored") self._attr_available = True - return - except RequestException as exc: if self.requester.available: LOGGER.warning("Connection failed, Obihai offline? %s", exc) + self._attr_native_value = None + self._attr_available = False + self.requester.available = False except IndexError as exc: if self.requester.available: LOGGER.warning("Connection failed, bad response: %s", exc) - - self._attr_native_value = None - self._attr_available = False - self.requester.available = False + self._attr_native_value = None + self._attr_available = False + self.requester.available = False diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 6bd592c38bf..f99a151292d 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -105,7 +105,9 @@ class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_api_key(self, user_input=None): """Get an Application Api Key.""" if not self.api_key_task: - self.api_key_task = self.hass.async_create_task(self._async_get_auth_key()) + self.api_key_task = self.hass.async_create_task( + self._async_get_auth_key(), eager_start=False + ) if not self.api_key_task.done(): return self.async_show_progress( step_id="get_api_key", @@ -115,11 +117,11 @@ class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.api_key_task - except OctoprintException as err: - _LOGGER.exception("Failed to get an application key: %s", err) + except OctoprintException: + _LOGGER.exception("Failed to get an application key") return self.async_show_progress_done(next_step_id="auth_failed") - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Failed to get an application key : %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to get an application key") return self.async_show_progress_done(next_step_id="auth_failed") finally: self.api_key_task = None @@ -133,7 +135,7 @@ class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry(existing_entry, data=user_input) # Reload the config entry otherwise devices will remain unavailable self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) + self.hass.config_entries.async_reload(existing_entry.entry_id), ) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py index 50d0667803f..e192aeb1fca 100644 --- a/homeassistant/components/ollama/config_flow.py +++ b/homeassistant/components/ollama/config_flow.py @@ -149,7 +149,8 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN): # 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}" + self.client.pull(self.model), + f"Downloading {self.model}", ) if self.download_task.done(): diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index 853370066dc..e25ae1f0877 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -81,75 +81,86 @@ 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", + "all-minilm", + "bakllava", + "codebooga", + "codegemma", + "codellama", + "codeqwen", + "codeup", + "command-r", + "command-r-plus", + "dbrx", + "deepseek-coder", + "deepseek-llm", + "dolphin-llama3", + "dolphin-mistral", + "dolphin-mixtral", + "dolphin-phi", + "dolphincoder", "duckdb-nsql", + "everythinglm", + "falcon", + "gemma", + "goliath", + "llama-pro", + "llama2", + "llama2-chinese", + "llama2-uncensored", + "llama3", + "llava", + "magicoder", + "meditron", + "medllama2", + "megadolphin", + "mistral", + "mistral-openorca", + "mistrallite", + "mixtral", + "mxbai-embed-large", + "neural-chat", + "nexusraven", + "nomic-embed-text", "notus", + "notux", + "nous-hermes", + "nous-hermes2", + "nous-hermes2-mixtral", + "open-orca-platypus2", + "openchat", + "openhermes", + "orca-mini", + "orca2", + "phi", + "phi3", + "phind-codellama", + "qwen", + "samantha-mistral", + "snowflake-arctic-embed", + "solar", + "sqlcoder", + "stable-beluga", + "stable-code", + "stablelm-zephyr", + "stablelm2", + "starcoder", + "starcoder2", + "starling-lm", + "tinydolphin", + "tinyllama", + "vicuna", + "wizard-math", + "wizard-vicuna", + "wizard-vicuna-uncensored", + "wizardcoder", + "wizardlm", + "wizardlm-uncensored", + "wizardlm2", + "xwinlm", + "yarn-llama2", + "yarn-mistral", + "yi", + "zephyr", ] DEFAULT_MODEL = "llama2:latest" diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index adc87e7be26..0484c889ba3 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -69,9 +69,7 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator[dict[tuple, dict[str, Any return data - parsed_data = get_item_data(data, "Backyard", (), parsed_data) - - return parsed_data + return get_item_data(data, "Backyard", (), parsed_data) class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index b3d59f50321..f960b1a8b81 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -5,12 +5,12 @@ from __future__ import annotations from datetime import timedelta import logging -from aiooncue import LoginFailedException, Oncue +from aiooncue import LoginFailedException, Oncue, OncueDevice 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.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -29,17 +29,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await client.async_login() except CONNECTION_EXCEPTIONS as ex: - raise ConfigEntryNotReady(ex) from ex + raise ConfigEntryNotReady from ex except LoginFailedException as ex: - _LOGGER.error("Failed to login to oncue service: %s", ex) - return False + raise ConfigEntryAuthFailed from ex + + async def _async_update() -> dict[str, OncueDevice]: + """Fetch data from Oncue.""" + try: + return await client.async_fetch_all() + except LoginFailedException as ex: + raise ConfigEntryAuthFailed from ex coordinator = DataUpdateCoordinator( hass, _LOGGER, name=f"Oncue {entry.data[CONF_USERNAME]}", update_interval=timedelta(minutes=10), - update_method=client.async_fetch_all, + update_method=_async_update, always_update=False, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/oncue/config_flow.py b/homeassistant/components/oncue/config_flow.py index ba672dcc588..e423ba08105 100644 --- a/homeassistant/components/oncue/config_flow.py +++ b/homeassistant/components/oncue/config_flow.py @@ -2,13 +2,14 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from aiooncue import LoginFailedException, Oncue import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,30 +23,26 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the oncue config flow.""" + self.reauth_entry: ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: - try: - await Oncue( - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - async_get_clientsession(self.hass), - ).async_login() - except CONNECTION_EXCEPTIONS: - errors["base"] = "cannot_connect" - except LoginFailedException: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: + if not (errors := await self._async_validate_or_error(user_input)): normalized_username = user_input[CONF_USERNAME].lower() await self.async_set_unique_id(normalized_username) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + ) return self.async_create_entry( title=normalized_username, data=user_input ) @@ -60,3 +57,54 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def _async_validate_or_error(self, config: dict[str, Any]) -> dict[str, str]: + """Validate the user input.""" + errors: dict[str, str] = {} + try: + await Oncue( + config[CONF_USERNAME], + config[CONF_PASSWORD], + async_get_clientsession(self.hass), + ).async_login() + except CONNECTION_EXCEPTIONS: + errors["base"] = "cannot_connect" + except LoginFailedException: + errors[CONF_PASSWORD] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return errors + + 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_USERNAME: existing_data[CONF_USERNAME] + } + if user_input is not None: + new_config = {**existing_data, CONF_PASSWORD: user_input[CONF_PASSWORD]} + if not (errors := await self._async_validate_or_error(new_config)): + return self.async_update_reload_and_abort( + existing_entry, data=new_config + ) + + return self.async_show_form( + description_placeholders=description_placeholders, + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + ) diff --git a/homeassistant/components/oncue/strings.json b/homeassistant/components/oncue/strings.json index f7a539fe0e6..ce7561962a2 100644 --- a/homeassistant/components/oncue/strings.json +++ b/homeassistant/components/oncue/strings.json @@ -6,6 +6,12 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Re-authenticate Oncue account {username}", + "data": { + "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%]" } } } diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index fea78fd3760..3c2ca3529cc 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -36,33 +36,33 @@ class OneWireBinarySensorEntityDescription( DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { "12": tuple( OneWireBinarySensorEntityDescription( - key=f"sensed.{id}", + key=f"sensed.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="sensed_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), "29": tuple( OneWireBinarySensorEntityDescription( - key=f"sensed.{id}", + key=f"sensed.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="sensed_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_7 + for device_key in DEVICE_KEYS_0_7 ), "3A": tuple( OneWireBinarySensorEntityDescription( - key=f"sensed.{id}", + key=f"sensed.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="sensed_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), "EF": (), # "HobbyBoard": special } @@ -71,15 +71,15 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { "HB_HUB": tuple( OneWireBinarySensorEntityDescription( - key=f"hub/short.{id}", + key=f"hub/short.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.PROBLEM, translation_key="hub_short_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ), } @@ -117,7 +117,7 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireBinarySensor]: device_type = device.type device_info = device.device_info device_sub_type = "std" - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index 9deaca2d121..a59953dcd25 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -16,4 +16,4 @@ class OWDeviceDescription: family: str id: str path: str - type: str + type: str | None diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index b01cc6ba3d6..2dc617ba039 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -45,7 +45,7 @@ DEVICE_MANUFACTURER = { _LOGGER = logging.getLogger(__name__) -def _is_known_device(device_family: str, device_type: str) -> bool: +def _is_known_device(device_family: str, device_type: str | None) -> bool: """Check if device family/type is known to the library.""" if device_family in ("7E", "EF"): # EDS or HobbyBoard return device_type in DEVICE_SUPPORT[device_family] @@ -144,11 +144,15 @@ class OneWireHub: return devices - def _get_device_type(self, device_path: str) -> str: + def _get_device_type(self, device_path: str) -> str | None: """Get device model.""" if TYPE_CHECKING: assert self.owproxy - device_type = self.owproxy.read(f"{device_path}type").decode() + try: + device_type = self.owproxy.read(f"{device_path}type").decode() + except protocol.ProtocolError as exc: + _LOGGER.debug("Unable to read `%stype`: %s", device_path, exc) + return None _LOGGER.debug("read `%stype`: %s", device_path, device_type) if device_type == "EDS": device_type = self.owproxy.read(f"{device_path}device_type").decode() diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index d32afce7fa9..3e43df4dddd 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -233,14 +233,14 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "42": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), "1D": tuple( OneWireSensorEntityDescription( - key=f"counter.{id}", + key=f"counter.{device_key}", native_unit_of_measurement="count", read_mode=READ_MODE_INT, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="counter_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), } @@ -273,15 +273,15 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), "HB_MOISTURE_METER": tuple( OneWireSensorEntityDescription( - key=f"moisture/sensor.{id}", + key=f"moisture/sensor.{device_key}", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.CBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, translation_key="moisture_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ), } @@ -377,10 +377,10 @@ def get_entities( device_info = device.device_info device_sub_type = "std" device_path = device.path - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type - elif "7E" in family: + elif device_type and "7E" in family: device_sub_type = "EDS" family = device_type elif "A6" in family: diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index cdf1315394e..94a7d41ab85 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -40,23 +40,23 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { "12": tuple( [ OneWireSwitchEntityDescription( - key=f"PIO.{id}", + key=f"PIO.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="pio_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ] + [ OneWireSwitchEntityDescription( - key=f"latch.{id}", + key=f"latch.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="latch_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ] ), "26": ( @@ -71,34 +71,34 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { "29": tuple( [ OneWireSwitchEntityDescription( - key=f"PIO.{id}", + key=f"PIO.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="pio_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_7 + for device_key in DEVICE_KEYS_0_7 ] + [ OneWireSwitchEntityDescription( - key=f"latch.{id}", + key=f"latch.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="latch_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_7 + for device_key in DEVICE_KEYS_0_7 ] ), "3A": tuple( OneWireSwitchEntityDescription( - key=f"PIO.{id}", + key=f"PIO.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="pio_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), "EF": (), # "HobbyBoard": special } @@ -108,37 +108,37 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { "HB_HUB": tuple( OneWireSwitchEntityDescription( - key=f"hub/branch.{id}", + key=f"hub/branch.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, translation_key="hub_branch_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ), "HB_MOISTURE_METER": tuple( [ OneWireSwitchEntityDescription( - key=f"moisture/is_leaf.{id}", + key=f"moisture/is_leaf.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, translation_key="leaf_sensor_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ] + [ OneWireSwitchEntityDescription( - key=f"moisture/is_moisture.{id}", + key=f"moisture/is_moisture.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, translation_key="moisture_sensor_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ] ), } @@ -178,7 +178,7 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireSwitch]: device_id = device.id device_info = device.device_info device_sub_type = "std" - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type elif "A6" in family: diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index ef0105bd6d2..7575443c793 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -143,7 +143,7 @@ def determine_zones(receiver): _LOGGER.debug("Zone 2 not available") except ValueError as error: if str(error) != TIMEOUT_MESSAGE: - raise error + raise _LOGGER.debug("Zone 2 timed out, assuming no functionality") try: _LOGGER.debug("Checking for zone 3 capability") @@ -154,7 +154,7 @@ def determine_zones(receiver): _LOGGER.debug("Zone 3 not available") except ValueError as error: if str(error) != TIMEOUT_MESSAGE: - raise error + raise _LOGGER.debug("Zone 3 timed out, assuming no functionality") except AssertionError: _LOGGER.error("Zone 3 detection failed") @@ -442,6 +442,7 @@ class OnkyoDevice(MediaPlayerEntity): "output_color_schema": _tuple_get(values, 6), "output_color_depth": _tuple_get(values, 7), "picture_mode": _tuple_get(values, 8), + "dynamic_range": _tuple_get(values, 9), } self._attr_extra_state_attributes[ATTR_VIDEO_INFORMATION] = info else: diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 515f9cd5f68..5bd81f2bdea 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -311,7 +311,7 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN): self.device_id = interface.Info.HwAddress except Fault as fault: if "not implemented" not in fault.message: - raise fault + raise LOGGER.debug( "%s: Could not get network interfaces: %s", self.onvif_config[CONF_NAME], diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 2001d95e2d4..b427cbda2f8 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -218,12 +218,13 @@ class ONVIFDevice: try: await device_mgmt.SetSystemDateAndTime(dt_param) LOGGER.debug("%s: SetSystemDateAndTime: success", self.name) - return # Some cameras don't support setting the timezone and will throw an IndexError # if we try to set it. If we get an error, try again without the timezone. except (IndexError, Fault): if idx == timezone_max_idx: raise + else: + return async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" @@ -344,7 +345,7 @@ class ONVIFDevice: mac = interface.Info.HwAddress except Fault as fault: if "not implemented" not in fault.message: - raise fault + raise LOGGER.debug( "Couldn't get network interfaces from ONVIF device '%s'. Error: %s", diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 690a3739b4f..29da0fee35f 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -221,9 +221,9 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None: None, payload.Data.SimpleItem[0].Value == "true", ) - return evt except (AttributeError, KeyError): return None + return evt @PARSERS.register("tns1:RuleEngine/CellMotionDetector/Motion") diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index ac09b0f61a2..e3bf763f429 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations from open_meteo import ( DailyParameters, Forecast, + HourlyParameters, OpenMeteo, OpenMeteoError, PrecipitationUnit, @@ -45,6 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DailyParameters.WIND_DIRECTION_10M_DOMINANT, DailyParameters.WIND_SPEED_10M_MAX, ], + hourly=[ + HourlyParameters.PRECIPITATION, + HourlyParameters.TEMPERATURE_2M, + HourlyParameters.WEATHER_CODE, + ], precipitation_unit=PrecipitationUnit.MILLIMETERS, temperature_unit=TemperatureUnit.CELSIUS, timezone="UTC", diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 8ee3edd5183..a2be81f0928 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -5,6 +5,12 @@ from __future__ import annotations from open_meteo import Forecast as OpenMeteoForecast from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, Forecast, SingleCoordinatorWeatherEntity, WeatherEntityFeature, @@ -15,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import DOMAIN, WMO_TO_HA_CONDITION_MAP @@ -39,7 +46,9 @@ class OpenMeteoWeatherEntity( _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__( self, @@ -95,31 +104,77 @@ class OpenMeteoWeatherEntity( return None forecasts: list[Forecast] = [] + daily = self.coordinator.data.daily - for index, time in enumerate(self.coordinator.data.daily.time): + for index, date in enumerate(self.coordinator.data.daily.time): forecast = Forecast( - datetime=time.isoformat(), + datetime=date.isoformat(), ) if daily.weathercode is not None: - forecast["condition"] = WMO_TO_HA_CONDITION_MAP.get( + forecast[ATTR_FORECAST_CONDITION] = WMO_TO_HA_CONDITION_MAP.get( daily.weathercode[index] ) if daily.precipitation_sum is not None: - forecast["native_precipitation"] = daily.precipitation_sum[index] + forecast[ATTR_FORECAST_NATIVE_PRECIPITATION] = daily.precipitation_sum[ + index + ] if daily.temperature_2m_max is not None: - forecast["native_temperature"] = daily.temperature_2m_max[index] + forecast[ATTR_FORECAST_NATIVE_TEMP] = daily.temperature_2m_max[index] if daily.temperature_2m_min is not None: - forecast["native_templow"] = daily.temperature_2m_min[index] + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = daily.temperature_2m_min[ + index + ] if daily.wind_direction_10m_dominant is not None: - forecast["wind_bearing"] = daily.wind_direction_10m_dominant[index] + forecast[ATTR_FORECAST_WIND_BEARING] = ( + daily.wind_direction_10m_dominant[index] + ) if daily.wind_speed_10m_max is not None: - forecast["native_wind_speed"] = daily.wind_speed_10m_max[index] + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = daily.wind_speed_10m_max[ + index + ] + + forecasts.append(forecast) + + return forecasts + + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + if self.coordinator.data.hourly is None: + return None + + forecasts: list[Forecast] = [] + + # Can have data in the past: https://github.com/open-meteo/open-meteo/issues/699 + today = dt_util.utcnow() + + hourly = self.coordinator.data.hourly + for index, datetime in enumerate(self.coordinator.data.hourly.time): + if dt_util.as_utc(datetime) < today: + continue + + forecast = Forecast( + datetime=datetime.isoformat(), + ) + + if hourly.weather_code is not None: + forecast[ATTR_FORECAST_CONDITION] = WMO_TO_HA_CONDITION_MAP.get( + hourly.weather_code[index] + ) + + if hourly.precipitation is not None: + forecast[ATTR_FORECAST_NATIVE_PRECIPITATION] = hourly.precipitation[ + index + ] + + if hourly.temperature_2m is not None: + forecast[ATTR_FORECAST_NATIVE_TEMP] = hourly.temperature_2m[index] forecasts.append(forecast) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 07e872a0f5d..ffbfc1799c5 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -2,53 +2,30 @@ from __future__ import annotations -import logging -from typing import Literal - import openai import voluptuous as vol from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, MATCH_ALL +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import ( - ConfigEntryNotReady, - HomeAssistantError, - TemplateError, -) +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import ( config_validation as cv, - intent, issue_registry as ir, selector, - template, ) from homeassistant.helpers.typing import ConfigType -from homeassistant.util import ulid -from .const import ( - CONF_CHAT_MODEL, - CONF_MAX_TOKENS, - CONF_PROMPT, - CONF_TEMPERATURE, - CONF_TOP_P, - DEFAULT_CHAT_MODEL, - DEFAULT_MAX_TOKENS, - DEFAULT_PROMPT, - DEFAULT_TEMPERATURE, - DEFAULT_TOP_P, - DOMAIN, -) +from .const import DOMAIN, LOGGER -_LOGGER = logging.getLogger(__name__) SERVICE_GENERATE_IMAGE = "generate_image" - +PLATFORMS = (Platform.CONVERSATION,) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -120,108 +97,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list) except openai.AuthenticationError as err: - _LOGGER.error("Invalid API key: %s", err) + LOGGER.error("Invalid API key: %s", err) return False except openai.OpenAIError as err: raise ConfigEntryNotReady(err) from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client - conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload OpenAI.""" + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + hass.data[DOMAIN].pop(entry.entry_id) conversation.async_unset_agent(hass, entry) return True - - -class OpenAIAgent(conversation.AbstractConversationAgent): - """OpenAI conversation agent.""" - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize the agent.""" - self.hass = hass - self.entry = entry - self.history: dict[str, list[dict]] = {} - - @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.""" - raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT) - model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) - max_tokens = self.entry.options.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS) - top_p = self.entry.options.get(CONF_TOP_P, DEFAULT_TOP_P) - temperature = self.entry.options.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) - - if user_input.conversation_id in self.history: - conversation_id = user_input.conversation_id - messages = self.history[conversation_id] - else: - conversation_id = ulid.ulid_now() - try: - prompt = self._async_generate_prompt(raw_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 with my template: {err}", - ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - messages = [{"role": "system", "content": prompt}] - - messages.append({"role": "user", "content": user_input.text}) - - _LOGGER.debug("Prompt for %s: %s", model, messages) - - client = self.hass.data[DOMAIN][self.entry.entry_id] - - try: - result = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=max_tokens, - top_p=top_p, - temperature=temperature, - user=conversation_id, - ) - except openai.OpenAIError as 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 OpenAI: {err}", - ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - - _LOGGER.debug("Response %s", result) - response = result.choices[0].message.model_dump(include={"role", "content"}) - messages.append(response) - self.history[conversation_id] = messages - - intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_speech(response["content"]) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - - def _async_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, - }, - parse_result=False, - ) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 46f8603c5f1..ee4a107c241 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -1,6 +1,9 @@ """Constants for the OpenAI Conversation integration.""" +import logging + DOMAIN = "openai_conversation" +LOGGER = logging.getLogger(__name__) CONF_PROMPT = "prompt" DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py new file mode 100644 index 00000000000..158b155c75d --- /dev/null +++ b/homeassistant/components/openai_conversation/conversation.py @@ -0,0 +1,145 @@ +"""Conversation support for OpenAI.""" + +from typing import Literal + +import openai + +from homeassistant.components import assist_pipeline, conversation +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import intent, template +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import ulid + +from .const import ( + CONF_CHAT_MODEL, + CONF_MAX_TOKENS, + CONF_PROMPT, + CONF_TEMPERATURE, + CONF_TOP_P, + DEFAULT_CHAT_MODEL, + DEFAULT_MAX_TOKENS, + DEFAULT_PROMPT, + DEFAULT_TEMPERATURE, + DEFAULT_TOP_P, + DOMAIN, + LOGGER, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up conversation entities.""" + agent = OpenAIConversationEntity(hass, config_entry) + async_add_entities([agent]) + + +class OpenAIConversationEntity( + conversation.ConversationEntity, conversation.AbstractConversationAgent +): + """OpenAI conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + self.history: dict[str, list[dict]] = {} + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id + + @property + def supported_languages(self) -> list[str] | Literal["*"]: + """Return a list of supported languages.""" + return MATCH_ALL + + async def async_added_to_hass(self) -> None: + """When entity is added to Home Assistant.""" + await super().async_added_to_hass() + assist_pipeline.async_migrate_engine( + self.hass, "conversation", self.entry.entry_id, self.entity_id + ) + conversation.async_set_agent(self.hass, self.entry, self) + + async def async_will_remove_from_hass(self) -> None: + """When entity will be removed from Home Assistant.""" + conversation.async_unset_agent(self.hass, self.entry) + await super().async_will_remove_from_hass() + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT) + model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) + max_tokens = self.entry.options.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS) + top_p = self.entry.options.get(CONF_TOP_P, DEFAULT_TOP_P) + temperature = self.entry.options.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) + + if user_input.conversation_id in self.history: + conversation_id = user_input.conversation_id + messages = self.history[conversation_id] + else: + conversation_id = ulid.ulid_now() + try: + prompt = self._async_generate_prompt(raw_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 with my template: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + messages = [{"role": "system", "content": prompt}] + + messages.append({"role": "user", "content": user_input.text}) + + LOGGER.debug("Prompt for %s: %s", model, messages) + + client = self.hass.data[DOMAIN][self.entry.entry_id] + + try: + result = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + top_p=top_p, + temperature=temperature, + user=conversation_id, + ) + except openai.OpenAIError as 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 OpenAI: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + LOGGER.debug("Response %s", result) + response = result.choices[0].message.model_dump(include={"role", "content"}) + messages.append(response) + self.history[conversation_id] = messages + + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_speech(response["content"]) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def _async_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, + }, + parse_result=False, + ) diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 5138be96b55..b71c84e2081 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -1,6 +1,7 @@ { "domain": "openai_conversation", "name": "OpenAI Conversation", + "after_dependencies": ["assist_pipeline"], "codeowners": ["@balloob"], "config_flow": true, "dependencies": ["conversation"], diff --git a/homeassistant/components/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py index 863b6050616..3cfd1ad30a0 100644 --- a/homeassistant/components/opensky/config_flow.py +++ b/homeassistant/components/opensky/config_flow.py @@ -100,19 +100,17 @@ class OpenSkyOptionsFlowHandler(OptionsFlowWithConfigEntry): if user_input[CONF_CONTRIBUTING_USER] and not authentication: errors["base"] = "no_authentication" if authentication and not errors: - async with OpenSky( - session=async_get_clientsession(self.hass) - ) as opensky: - try: - await opensky.authenticate( - BasicAuth( - login=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ), - contributing_user=user_input[CONF_CONTRIBUTING_USER], - ) - except OpenSkyUnauthenticatedError: - errors["base"] = "invalid_auth" + opensky = OpenSky(session=async_get_clientsession(self.hass)) + try: + await opensky.authenticate( + BasicAuth( + login=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ), + contributing_user=user_input[CONF_CONTRIBUTING_USER], + ) + except OpenSkyUnauthenticatedError: + errors["base"] = "invalid_auth" if not errors: return self.async_create_entry( title=self.options.get(CONF_NAME, "OpenSky"), diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 7c018e20a36..6357ce38e1d 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -14,10 +14,9 @@ async def async_get_scanner( ) -> OPNSenseDeviceScanner: """Configure the OPNSense device_tracker.""" interface_client = hass.data[OPNSENSE_DATA]["interfaces"] - scanner = OPNSenseDeviceScanner( + return OPNSenseDeviceScanner( interface_client, hass.data[OPNSENSE_DATA][CONF_TRACKER_INTERFACE] ) - return scanner class OPNSenseDeviceScanner(DeviceScanner): @@ -46,8 +45,7 @@ class OPNSenseDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" if device not in self.last_results: return None - hostname = self.last_results[device].get("hostname") or None - return hostname + return self.last_results[device].get("hostname") or None def update_info(self): """Ensure the information from the OPNSense router is up to date. diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index d4cce99e1cc..94a56bb1922 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -159,13 +159,9 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) ) - name_prefix = " ".join( - ( - "Opower", - self.api.utility.subdomain(), - account.meter_type.name.lower(), - account.utility_account_id, - ) + name_prefix = ( + f"Opower {self.api.utility.subdomain()} " + f"{account.meter_type.name.lower()} {account.utility_account_id}" ) cost_metadata = StatisticMetaData( has_mean=False, diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 51ad669733b..91e4fbc960c 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.3"] + "requirements": ["opower==0.4.4"] } diff --git a/homeassistant/components/oralb/strings.json b/homeassistant/components/oralb/strings.json index f60fd56a9a4..775bbedac74 100644 --- a/homeassistant/components/oralb/strings.json +++ b/homeassistant/components/oralb/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/osoenergy/__init__.py b/homeassistant/components/osoenergy/__init__.py index 48ea01e8bb8..20ff22cea23 100644 --- a/homeassistant/components/osoenergy/__init__.py +++ b/homeassistant/components/osoenergy/__init__.py @@ -16,18 +16,25 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from .const import DOMAIN -_T = TypeVar( - "_T", OSOEnergyBinarySensorData, OSOEnergySensorData, OSOEnergyWaterHeaterData +_OSOEnergyT = TypeVar( + "_OSOEnergyT", + OSOEnergyBinarySensorData, + OSOEnergySensorData, + OSOEnergyWaterHeaterData, ) +MANUFACTURER = "OSO Energy" PLATFORMS = [ + Platform.SENSOR, Platform.WATER_HEATER, ] PLATFORM_LOOKUP = { + Platform.SENSOR: "sensor", Platform.WATER_HEATER: "water_heater", } @@ -70,13 +77,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class OSOEnergyEntity(Entity, Generic[_T]): +class OSOEnergyEntity(Entity, Generic[_OSOEnergyT]): """Initiate OSO Energy Base Class.""" _attr_has_entity_name = True - def __init__(self, osoenergy: OSOEnergy, osoenergy_device: _T) -> None: + def __init__(self, osoenergy: OSOEnergy, entity_data: _OSOEnergyT) -> None: """Initialize the instance.""" self.osoenergy = osoenergy - self.device = osoenergy_device - self._attr_unique_id = osoenergy_device.device_id + self.entity_data = entity_data + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entity_data.device_id)}, + manufacturer=MANUFACTURER, + model=entity_data.device_type, + name=entity_data.device_name, + ) diff --git a/homeassistant/components/osoenergy/sensor.py b/homeassistant/components/osoenergy/sensor.py new file mode 100644 index 00000000000..0be6ad83281 --- /dev/null +++ b/homeassistant/components/osoenergy/sensor.py @@ -0,0 +1,151 @@ +"""Support for OSO Energy sensors.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from apyosoenergyapi import OSOEnergy +from apyosoenergyapi.helper.const import OSOEnergySensorData + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import OSOEnergyEntity +from .const import DOMAIN + + +@dataclass(frozen=True, kw_only=True) +class OSOEnergySensorEntityDescription(SensorEntityDescription): + """Class describing OSO Energy heater sensor entities.""" + + value_fn: Callable[[OSOEnergy], StateType] + + +SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = { + "heater_mode": OSOEnergySensorEntityDescription( + key="heater_mode", + translation_key="heater_mode", + device_class=SensorDeviceClass.ENUM, + options=[ + "auto", + "manual", + "off", + "legionella", + "powersave", + "extraenergy", + "voltage", + "ffr", + ], + value_fn=lambda entity_data: entity_data.state.lower(), + ), + "optimization_mode": OSOEnergySensorEntityDescription( + key="optimization_mode", + translation_key="optimization_mode", + device_class=SensorDeviceClass.ENUM, + options=["off", "oso", "gridcompany", "smartcompany", "advanced"], + value_fn=lambda entity_data: entity_data.state.lower(), + ), + "power_load": OSOEnergySensorEntityDescription( + key="power_load", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + value_fn=lambda entity_data: entity_data.state, + ), + "tapping_capacity": OSOEnergySensorEntityDescription( + key="tapping_capacity", + translation_key="tapping_capacity", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value_fn=lambda entity_data: entity_data.state, + ), + "capacity_mixed_water_40": OSOEnergySensorEntityDescription( + key="capacity_mixed_water_40", + translation_key="capacity_mixed_water_40", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "v40_min": OSOEnergySensorEntityDescription( + key="v40_min", + translation_key="v40_min", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "v40_level_min": OSOEnergySensorEntityDescription( + key="v40_level_min", + translation_key="v40_level_min", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "v40_level_max": OSOEnergySensorEntityDescription( + key="v40_level_max", + translation_key="v40_level_max", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "volume": OSOEnergySensorEntityDescription( + key="volume", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up OSO Energy sensor.""" + osoenergy = hass.data[DOMAIN][entry.entry_id] + devices = osoenergy.session.device_list.get("sensor") + entities = [] + if devices: + for dev in devices: + sensor_type = dev.osoEnergyType.lower() + if sensor_type in SENSOR_TYPES: + entities.append( + OSOEnergySensor(osoenergy, SENSOR_TYPES[sensor_type], dev) + ) + + async_add_entities(entities, True) + + +class OSOEnergySensor(OSOEnergyEntity[OSOEnergySensorData], SensorEntity): + """OSO Energy Sensor Entity.""" + + entity_description: OSOEnergySensorEntityDescription + + def __init__( + self, + instance: OSOEnergy, + description: OSOEnergySensorEntityDescription, + entity_data: OSOEnergySensorData, + ) -> None: + """Initialize the OSO Energy sensor.""" + super().__init__(instance, entity_data) + + device_id = entity_data.device_id + self._attr_unique_id = f"{device_id}_{description.key}" + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.entity_data) + + async def async_update(self) -> None: + """Update all data for OSO Energy.""" + await self.osoenergy.session.update_data() + self.entity_data = await self.osoenergy.sensor.get_sensor(self.entity_data) diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index a45482bf030..5313f1d6565 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -17,13 +17,56 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "tapping_capacity": { + "name": "Tapping capacity" + }, + "capacity_mixed_water_40": { + "name": "Capacity mixed water 40°C" + }, + "v40_min": { + "name": "Mixed water at 40°C" + }, + "v40_level_min": { + "name": "Minimum level of mixed water at 40°C" + }, + "v40_level_max": { + "name": "Maximum level of mixed water at 40°C" + }, + "heater_mode": { + "name": "Heater mode", + "state": { + "auto": "Auto", + "extraenergy": "Extra energy", + "ffr": "Fast frequency reserve", + "legionella": "Legionella", + "manual": "Manual", + "off": "Off", + "powersave": "Power save", + "voltage": "Voltage" + } + }, + "optimization_mode": { + "name": "Optimization mode", + "state": { + "advanced": "Advanced", + "gridcompany": "Grid company", + "off": "Off", + "oso": "OSO", + "smartcompany": "Smart company" + } + }, + "profile": { + "name": "Profile local" + } + } } } diff --git a/homeassistant/components/osoenergy/water_heater.py b/homeassistant/components/osoenergy/water_heater.py index eaf54a9f9a4..b7fb2ba16e6 100644 --- a/homeassistant/components/osoenergy/water_heater.py +++ b/homeassistant/components/osoenergy/water_heater.py @@ -2,6 +2,7 @@ from typing import Any +from apyosoenergyapi import OSOEnergy from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData from homeassistant.components.water_heater import ( @@ -15,7 +16,6 @@ from homeassistant.components.water_heater import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import OSOEnergyEntity @@ -34,9 +34,6 @@ CURRENT_OPERATION_MAP: dict[str, Any] = { "extraenergy": STATE_HIGH_DEMAND, }, } -HEATER_MIN_TEMP = 10 -HEATER_MAX_TEMP = 80 -MANUFACTURER = "OSO Energy" async def async_setup_entry( @@ -59,30 +56,29 @@ class OSOEnergyWaterHeater( _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device.device_id)}, - manufacturer=MANUFACTURER, - model=self.device.device_type, - name=self.device.device_name, - ) + def __init__( + self, + instance: OSOEnergy, + entity_data: OSOEnergyWaterHeaterData, + ) -> None: + """Initialize the OSO Energy water heater.""" + super().__init__(instance, entity_data) + self._attr_unique_id = entity_data.device_id @property def available(self) -> bool: """Return if the device is available.""" - return self.device.available + return self.entity_data.available @property def current_operation(self) -> str: """Return current operation.""" - status = self.device.current_operation + status = self.entity_data.current_operation if status == "off": return STATE_OFF - optimization_mode = self.device.optimization_mode.lower() - heater_mode = self.device.heater_mode.lower() + optimization_mode = self.entity_data.optimization_mode.lower() + heater_mode = self.entity_data.heater_mode.lower() if optimization_mode in CURRENT_OPERATION_MAP: return CURRENT_OPERATION_MAP[optimization_mode].get( heater_mode, STATE_ELECTRIC @@ -93,49 +89,51 @@ class OSOEnergyWaterHeater( @property def current_temperature(self) -> float: """Return the current temperature of the heater.""" - return self.device.current_temperature + return self.entity_data.current_temperature @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self.device.target_temperature + return self.entity_data.target_temperature @property def target_temperature_high(self) -> float: """Return the temperature we try to reach.""" - return self.device.target_temperature_high + return self.entity_data.target_temperature_high @property def target_temperature_low(self) -> float: """Return the temperature we try to reach.""" - return self.device.target_temperature_low + return self.entity_data.target_temperature_low @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.device.min_temperature + return self.entity_data.min_temperature @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.device.max_temperature + return self.entity_data.max_temperature async def async_turn_on(self, **kwargs) -> None: """Turn on hotwater.""" - await self.osoenergy.hotwater.turn_on(self.device, True) + await self.osoenergy.hotwater.turn_on(self.entity_data, True) async def async_turn_off(self, **kwargs) -> None: """Turn off hotwater.""" - await self.osoenergy.hotwater.turn_off(self.device, True) + await self.osoenergy.hotwater.turn_off(self.entity_data, True) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temperature = int(kwargs.get("temperature", self.target_temperature)) profile = [target_temperature] * 24 - await self.osoenergy.hotwater.set_profile(self.device, profile) + await self.osoenergy.hotwater.set_profile(self.entity_data, profile) async def async_update(self) -> None: """Update all Node data from Hive.""" await self.osoenergy.session.update_data() - self.device = await self.osoenergy.hotwater.get_water_heater(self.device) + self.entity_data = await self.osoenergy.hotwater.get_water_heater( + self.entity_data + ) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 2f0ba1bccb4..50696530e8a 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -83,8 +83,8 @@ def setup_platform( host = config[CONF_HOST] try: bridge = Lightify(host, log_level=logging.NOTSET) - except OSError as err: - _LOGGER.exception("Error connecting to bridge: %s due to: %s", host, err) + except OSError: + _LOGGER.exception("Error connecting to bridge %s", host) return setup_bridge(bridge, add_entities, config) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index f95f885f7ef..eb79910d63f 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -368,12 +368,10 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN): self, username: str, password: str, server: OverkizServer ) -> OverkizClient: session = async_create_clientsession(self.hass) - client = OverkizClient( + return OverkizClient( username=username, password=password, server=server, session=session ) - return client - async def _create_local_api_token( self, cloud_client: OverkizClient, host: str, verify_ssl: bool ) -> str: diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 2ef0f0ebef4..dc2f0df4783 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.9"], + "requirements": ["pyoverkiz==1.13.10"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 6ad39ad82cb..d207f3161f4 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -7,13 +7,14 @@ from datetime import timedelta import logging import aiohttp -from ovoenergy import OVODailyUsage -from ovoenergy.ovoenergy import OVOEnergy +from ovoenergy import OVOEnergy +from ovoenergy.models import OVODailyUsage from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -32,29 +33,35 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" - client = OVOEnergy() + client = OVOEnergy( + client_session=async_get_clientsession(hass), + ) + + if custom_account := entry.data.get(CONF_ACCOUNT) is not None: + client.custom_account_id = custom_account try: - authenticated = await client.authenticate( + if not await client.authenticate( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], - entry.data[CONF_ACCOUNT], - ) + ): + raise ConfigEntryAuthFailed + + await client.bootstrap_accounts() except aiohttp.ClientError as exception: _LOGGER.warning(exception) raise ConfigEntryNotReady from exception - if not authenticated: - raise ConfigEntryAuthFailed - async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" + if custom_account := entry.data.get(CONF_ACCOUNT) is not None: + client.custom_account_id = custom_account + async with asyncio.timeout(10): try: authenticated = await client.authenticate( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], - entry.data[CONF_ACCOUNT], ) except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 41c64913764..87d53e5fbf9 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -6,11 +6,12 @@ from collections.abc import Mapping from typing import Any import aiohttp -from ovoenergy.ovoenergy import OVOEnergy +from ovoenergy import OVOEnergy import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_ACCOUNT, DOMAIN @@ -41,13 +42,19 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" errors = {} if user_input is not None: - client = OVOEnergy() + client = OVOEnergy( + client_session=async_get_clientsession(self.hass), + ) + + if custom_account := user_input.get(CONF_ACCOUNT) is not None: + client.custom_account_id = custom_account + try: authenticated = await client.authenticate( user_input[CONF_USERNAME], user_input[CONF_PASSWORD], - user_input.get(CONF_ACCOUNT, None), ) + await client.bootstrap_accounts() except aiohttp.ClientError: errors["base"] = "cannot_connect" else: @@ -86,10 +93,17 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {CONF_USERNAME: self.username} if user_input is not None and user_input.get(CONF_PASSWORD) is not None: - client = OVOEnergy() + client = OVOEnergy( + client_session=async_get_clientsession(self.hass), + ) + + if self.account is not None: + client.custom_account_id = self.account + try: authenticated = await client.authenticate( - self.username, user_input[CONF_PASSWORD], self.account + self.username, + user_input[CONF_PASSWORD], ) except aiohttp.ClientError: errors["base"] = "connection_error" diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 87e356417b0..af4a313206e 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"], - "requirements": ["ovoenergy==1.2.0"] + "requirements": ["ovoenergy==2.0.0"] } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 76c084b368f..3012a130a1a 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -7,8 +7,8 @@ import dataclasses from datetime import datetime, timedelta from typing import Final -from ovoenergy import OVODailyUsage -from ovoenergy.ovoenergy import OVOEnergy +from ovoenergy import OVOEnergy +from ovoenergy.models import OVODailyUsage from homeassistant.components.sensor import ( SensorDeviceClass, @@ -54,7 +54,7 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( key=KEY_LAST_ELECTRICITY_COST, translation_key=KEY_LAST_ELECTRICITY_COST, device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, value=lambda usage: usage.electricity[-1].cost.amount if usage.electricity[-1].cost is not None else None, @@ -88,7 +88,7 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( key=KEY_LAST_GAS_COST, translation_key=KEY_LAST_GAS_COST, device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, value=lambda usage: usage.gas[-1].cost.amount if usage.gas[-1].cost is not None else None, diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 8471f734196..31af3d845ae 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -30,9 +30,8 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) for identifier in device.identifiers - if identifier[0] == OT_DOMAIN } entities = [] diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 3e669079848..011b4f75489 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -135,8 +135,6 @@ def _decrypt_payload(secret, topic, ciphertext): try: message = decrypt(ciphertext, key) message = message.decode("utf-8") - _LOGGER.debug("Decrypted payload: %s", message) - return message except ValueError: _LOGGER.warning( ( @@ -146,6 +144,8 @@ def _decrypt_payload(secret, topic, ciphertext): topic, ) return None + _LOGGER.debug("Decrypted payload: %s", message) + return message def encrypt_message(secret, topic, message): diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 7d224c7126f..5c76a7e6900 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -177,8 +177,8 @@ class Remote: self._control = None self.state = STATE_OFF self.available = self._on_action is not None - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("An unknown error occurred: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred") self._control = None self.state = STATE_OFF self.available = self._on_action is not None @@ -247,9 +247,6 @@ class Remote: """Handle errors from func, set available and reconnect if needed.""" try: result = await self._hass.async_add_executor_job(func, *args) - self.state = STATE_ON - self.available = True - return result except EncryptionRequired: _LOGGER.error( "The connection couldn't be encrypted. Please reconfigure your TV" @@ -260,12 +257,18 @@ class Remote: self.state = STATE_OFF self.available = True await self.async_create_remote_control() + return None except (URLError, OSError) as err: _LOGGER.debug("An error occurred: %s", err) self.state = STATE_OFF self.available = self._on_action is not None await self.async_create_remote_control() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("An unknown error occurred: %s", err) + return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred") self.state = STATE_OFF self.available = self._on_action is not None + return None + self.state = STATE_ON + self.available = True + return result diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index c06de119244..65a830c9b1a 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -60,8 +60,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except (URLError, SOAPError, OSError) as err: _LOGGER.error("Could not establish remote connection: %s", err) errors["base"] = "cannot_connect" - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("An unknown error occurred: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred") return self.async_abort(reason="unknown") if "base" not in errors: @@ -118,8 +118,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except (URLError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason="cannot_connect") - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Unknown error: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") return self.async_abort(reason="unknown") if "base" not in errors: @@ -142,8 +142,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except (URLError, SOAPError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason="cannot_connect") - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Unknown error: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") return self.async_abort(reason="unknown") return self.async_show_form( diff --git a/homeassistant/components/pegel_online/manifest.json b/homeassistant/components/pegel_online/manifest.json index d193fd7487a..d51278d0c1b 100644 --- a/homeassistant/components/pegel_online/manifest.json +++ b/homeassistant/components/pegel_online/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aiopegelonline"], - "requirements": ["aiopegelonline==0.0.9"] + "requirements": ["aiopegelonline==0.0.10"] } diff --git a/homeassistant/components/permobil/coordinator.py b/homeassistant/components/permobil/coordinator.py index f505e73fa23..6efde26d341 100644 --- a/homeassistant/components/permobil/coordinator.py +++ b/homeassistant/components/permobil/coordinator.py @@ -50,8 +50,7 @@ class MyPermobilCoordinator(DataUpdateCoordinator[MyPermobilData]): except MyPermobilAPIException as err: _LOGGER.exception( - "Error fetching data from MyPermobil API for account %s %s", + "Error fetching data from MyPermobil API for account %s", self.p_api.email, - err, ) raise UpdateFailed from err diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 8aa3251641b..4f86654a7d3 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable import logging from typing import Any, Self @@ -17,7 +17,6 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( ATTR_EDITABLE, - ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ID, ATTR_LATITUDE, @@ -35,6 +34,7 @@ from homeassistant.const import ( ) from homeassistant.core import ( Event, + EventStateChangedData, HomeAssistant, ServiceCall, State, @@ -48,10 +48,7 @@ from homeassistant.helpers import ( service, ) from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -244,20 +241,23 @@ 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_data: Mapping[str, Any]) -> bool: + def _entity_registry_filter( + self, event_data: er.EventEntityRegistryUpdatedData + ) -> bool: """Filter entity registry events.""" return ( event_data["action"] == "remove" - and split_entity_id(event_data[ATTR_ENTITY_ID])[0] == "device_tracker" + and split_entity_id(event_data["entity_id"])[0] == "device_tracker" ) - async def _entity_registry_updated(self, event: Event) -> None: + async def _entity_registry_updated( + self, event: Event[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle entity registry updated.""" - entity_id = event.data[ATTR_ENTITY_ID] + entity_id = event.data["entity_id"] for person in list(self.data.values()): if entity_id not in person[CONF_DEVICE_TRACKERS]: continue @@ -404,7 +404,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Person(collection.CollectionEntity, RestoreEntity): +class Person( + collection.CollectionEntity, + RestoreEntity, +): """Represent a tracked person.""" _entity_component_unrecorded_attributes = frozenset({ATTR_DEVICE_TRACKERS}) @@ -419,8 +422,18 @@ class Person(collection.CollectionEntity, RestoreEntity): self._longitude: float | None = None self._gps_accuracy: float | None = None self._source: str | None = None - self._state: str | None = None self._unsub_track_device: Callable[[], None] | None = None + self._attr_state: str | None = None + self.device_trackers: list[str] = [] + + self._attr_unique_id = config[CONF_ID] + self._set_attrs_from_config() + + def _set_attrs_from_config(self) -> None: + """Set attributes from config.""" + self._attr_name = self._config[CONF_NAME] + self._attr_entity_picture = self._config.get(CONF_PICTURE) + self.device_trackers = self._config[CONF_DEVICE_TRACKERS] @classmethod def from_storage(cls, config: ConfigType) -> Self: @@ -436,48 +449,6 @@ class Person(collection.CollectionEntity, RestoreEntity): person.editable = False return person - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._config[CONF_NAME] - - @property - def entity_picture(self) -> str | None: - """Return entity picture.""" - return self._config.get(CONF_PICTURE) - - @property - def state(self) -> str | None: - """Return the state of the person.""" - return self._state - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the person.""" - data: dict[str, Any] = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id} - if self._latitude is not None: - data[ATTR_LATITUDE] = self._latitude - if self._longitude is not None: - data[ATTR_LONGITUDE] = self._longitude - if self._gps_accuracy is not None: - data[ATTR_GPS_ACCURACY] = self._gps_accuracy - if self._source is not None: - data[ATTR_SOURCE] = self._source - if (user_id := self._config.get(CONF_USER_ID)) is not None: - data[ATTR_USER_ID] = user_id - data[ATTR_DEVICE_TRACKERS] = self.device_trackers - return data - - @property - def unique_id(self) -> str: - """Return a unique ID for the person.""" - return self._config[CONF_ID] - - @property - def device_trackers(self) -> list[str]: - """Return the device trackers for the person.""" - return self._config[CONF_DEVICE_TRACKERS] - async def async_added_to_hass(self) -> None: """Register device trackers.""" await super().async_added_to_hass() @@ -497,6 +468,9 @@ class Person(collection.CollectionEntity, RestoreEntity): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, _async_person_start_hass ) + # Update extra state attributes now + # as there are attributes that can already be set + self._update_extra_state_attributes() async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" @@ -506,6 +480,7 @@ class Person(collection.CollectionEntity, RestoreEntity): def _async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config + self._set_attrs_from_config() if self._unsub_track_device is not None: self._unsub_track_device() @@ -552,12 +527,13 @@ class Person(collection.CollectionEntity, RestoreEntity): if latest: self._parse_source_state(latest) else: - self._state = None + self._attr_state = None self._source = None self._latitude = None self._longitude = None self._gps_accuracy = None + self._update_extra_state_attributes() self.async_write_ha_state() @callback @@ -566,12 +542,34 @@ class Person(collection.CollectionEntity, RestoreEntity): This is a device tracker state or the restored person state. """ - self._state = state.state + self._attr_state = state.state self._source = state.entity_id self._latitude = state.attributes.get(ATTR_LATITUDE) self._longitude = state.attributes.get(ATTR_LONGITUDE) self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) + @callback + def _update_extra_state_attributes(self) -> None: + """Update extra state attributes.""" + data: dict[str, Any] = { + ATTR_EDITABLE: self.editable, + ATTR_ID: self.unique_id, + ATTR_DEVICE_TRACKERS: self.device_trackers, + } + + if self._latitude is not None: + data[ATTR_LATITUDE] = self._latitude + if self._longitude is not None: + data[ATTR_LONGITUDE] = self._longitude + if self._gps_accuracy is not None: + data[ATTR_GPS_ACCURACY] = self._gps_accuracy + if self._source is not None: + data[ATTR_SOURCE] = self._source + if (user_id := self._config.get(CONF_USER_ID)) is not None: + data[ATTR_USER_ID] = user_id + + self._attr_extra_state_attributes = data + @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) def ws_list_person( diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 99439ba3a17..975d8a1494c 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -1,7 +1,7 @@ { "domain": "pi_hole", "name": "Pi-hole", - "codeowners": ["@johnluetke", "@shenxn"], + "codeowners": ["@shenxn"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pi_hole", "iot_class": "local_polling", diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 7a76d3174cd..c367d5ec548 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -50,14 +50,14 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): # Update the auth token in the config entry if applicable self._update_auth_token() - - # Return the fetched data - return data except ValueError as error: raise UpdateFailed(f"API response was malformed: {error}") from error except PicnicAuthError as error: raise ConfigEntryAuthFailed from error + # Return the fetched data + return data + def fetch_data(self): """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" # Fetch from the API and pre-process the data diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index f9afcef7be9..f1fd8518d42 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -140,7 +140,6 @@ class PingDataSubProcess(PingData): if TYPE_CHECKING: assert match is not None rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() - return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} except TimeoutError: _LOGGER.exception( "Timed out running command: `%s`, after: %ss", @@ -155,6 +154,7 @@ class PingDataSubProcess(PingData): return None except AttributeError: return None + return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} async def async_update(self) -> None: """Retrieve the latest details from the host.""" diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 0d8678d95ef..c68e2c8ad75 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -234,8 +234,7 @@ class PlaatoCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce- async def _async_update_data(self): """Update data via library.""" - data = await self.api.get_data( + return await self.api.get_data( session=aiohttp_client.async_get_clientsession(self.hass), device_type=self.device_type, ) - return data diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 076f93faf7b..4f35f9eb281 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -20,15 +20,18 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 301716e14d5..dabde0b0490 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -216,8 +216,8 @@ class PlexFlowHandler(ConfigFlow, domain=DOMAIN): self.available_servers = available_servers.args[0] return await self.async_step_select_server() - except Exception as error: # pylint: disable=broad-except - _LOGGER.exception("Unknown error connecting to Plex server: %s", error) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error connecting to Plex server") return self.async_abort(reason="unknown") if errors: diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 28389ffa357..3140e518688 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -49,8 +49,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: """Migrate Plugwise entity entries. - - Migrates unique ID from old relay switches to the new unique ID + - Migrates old unique ID's from old binary_sensors and switches to the new unique ID's """ + if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith( + "-slave_boiler_state" + ): + return { + "new_unique_id": entry.unique_id.replace( + "-slave_boiler_state", "-secondary_boiler_state" + ) + } if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"): return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")} diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index d32ae94160f..01ebc736dbe 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -64,8 +64,8 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( - key="slave_boiler_state", - translation_key="slave_boiler_state", + key="secondary_boiler_state", + translation_key="secondary_boiler_state", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 888f813760a..ada7d2d2533 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["plugwise"], - "requirements": ["plugwise==0.37.1"], + "requirements": ["plugwise==0.37.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 7d26f5a624c..ef2d6458441 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -48,7 +48,7 @@ "cooling_state": { "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]" }, - "slave_boiler_state": { + "secondary_boiler_state": { "name": "Secondary boiler state" }, "plugwise_notification": { diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 5257e5a6299..e9334edb6d5 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -5,7 +5,6 @@ from __future__ import annotations from contextlib import AsyncExitStack from datetime import timedelta import logging -from typing import Optional from aiohttp import CookieJar from tesla_powerwall import ( @@ -244,7 +243,7 @@ async def _call_base_info(power_wall: Powerwall, host: str) -> PowerwallBaseInfo ) -async def get_backup_reserve_percentage(power_wall: Powerwall) -> Optional[float]: +async def get_backup_reserve_percentage(power_wall: Powerwall) -> float | None: """Return the backup reserve percentage.""" try: return await power_wall.get_backup_reserve_percentage() diff --git a/homeassistant/components/private_ble_device/strings.json b/homeassistant/components/private_ble_device/strings.json index 9e20a9476ec..c35775a4843 100644 --- a/homeassistant/components/private_ble_device/strings.json +++ b/homeassistant/components/private_ble_device/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "What is the IRK (Identity Resolving Key) of the BLE device you want to track?", diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 0ec931ceade..c02cbeabd84 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State +from homeassistant.core import Event, EventStateChangedData, 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 +57,6 @@ from homeassistant.helpers.entity_registry import ( EventEntityRegistryUpdatedData, ) from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import as_timestamp from homeassistant.util.unit_conversion import TemperatureConverter diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index ca8e4db35cc..911ae6104fd 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -35,11 +35,11 @@ async def validate_input(hass: HomeAssistant, data): auth = Auth(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY]) try: contracts = await Installation.list(auth) - return auth, contracts except ConnectionRefusedError: raise InvalidAuth from ConnectionRefusedError except ConnectionError: raise CannotConnect from ConnectionError + return auth, contracts class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN): @@ -62,8 +62,8 @@ class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: self.user_input = user_input diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index f6c67fc088f..d739efe39e7 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_entity_registry_updated_event, - async_track_state_change, + async_track_state_change_event, ) from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = ProximityDataUpdateCoordinator(hass, entry.title, dict(entry.data)) entry.async_on_unload( - async_track_state_change( + async_track_state_change_event( hass, entry.data[CONF_TRACKED_ENTITIES], coordinator.async_check_proximity_state_change, diff --git a/homeassistant/components/proximity/coordinator.py b/homeassistant/components/proximity/coordinator.py index ea33c1f8121..ff7eedb5cd0 100644 --- a/homeassistant/components/proximity/coordinator.py +++ b/homeassistant/components/proximity/coordinator.py @@ -15,7 +15,13 @@ from homeassistant.const import ( CONF_ZONE, UnitOfLength, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + 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 @@ -100,10 +106,14 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): self.entity_mapping[tracked_entity_id].append(entity_id) async def async_check_proximity_state_change( - self, entity: str, old_state: State | None, new_state: State | None + self, + event: Event[EventStateChangedData], ) -> None: """Fetch and process state change event.""" - self.state_change_data = StateChangedData(entity, old_state, new_state) + data = event.data + self.state_change_data = StateChangedData( + data["entity_id"], data["old_state"], data["new_state"] + ) await self.async_refresh() async def async_check_tracked_entity_change( diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index e0b8f91088d..6d6771debc4 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -200,8 +200,7 @@ def create_coordinator_container_vm( def poll_api() -> dict[str, Any] | None: """Call the api.""" - vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type) - return vm_status + return call_api_container_vm(proxmox, node_name, vm_id, vm_type) vm_status = await hass.async_add_executor_job(poll_api) diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 604b029fc92..e8d357726bc 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -146,15 +146,19 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { translation_key="progress", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(float, data["progress"]), - available_fn=lambda data: data.get("progress") is not None - and data.get("state") != PrinterState.IDLE.value, + 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", value_fn=lambda data: cast(str, data["file"]["display_name"]), - available_fn=lambda data: data.get("file") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("file") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.start", @@ -164,8 +168,10 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { lambda data: (utcnow() - timedelta(seconds=data["time_printing"])), timedelta(minutes=2), ), - available_fn=lambda data: data.get("time_printing") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("time_printing") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.finish", @@ -175,8 +181,10 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { lambda data: (utcnow() + timedelta(seconds=data["time_remaining"])), timedelta(minutes=2), ), - available_fn=lambda data: data.get("time_remaining") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("time_remaining") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), ), } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index f01bc00ba72..77477ba7901 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -350,16 +350,17 @@ class PS4Device(MediaPlayerEntity): self._attr_unique_id = entry.unique_id self.entity_id = entry.entity_id break - for device in d_registry.devices.values(): - if self._entry_id in device.config_entries: - self._attr_device_info = DeviceInfo( - identifiers=device.identifiers, - manufacturer=device.manufacturer, - model=device.model, - name=device.name, - sw_version=device.sw_version, - ) - break + for device in d_registry.devices.get_devices_for_config_entry_id( + self._entry_id + ): + self._attr_device_info = DeviceInfo( + identifiers=device.identifiers, + manufacturer=device.manufacturer, + model=device.model, + name=device.name, + sw_version=device.sw_version, + ) + break else: _sw_version = status["system-version"] diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index f9a6415bb38..5ba88318a1c 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -25,17 +25,14 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SHOW_ON_MAP, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -115,8 +112,9 @@ def async_get_remove_sensor_options( device_registry = dr.async_get(hass) return [ SelectOptionDict(value=device_entry.id, label=cast(str, device_entry.name)) - for device_entry in device_registry.devices.values() - if config_entry.entry_id in device_entry.config_entries + for device_entry in device_registry.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) ] diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index e976ae5d1b0..89e9eb5a9eb 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -290,7 +290,7 @@ def execute(hass, filename, source, data=None, return_response=False): raise HomeAssistantError( f"Error executing script ({type(err).__name__}): {err}" ) from err - logger.exception("Error executing script: %s", err) + logger.exception("Error executing script") return None return restricted_globals["output"] diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index 7b1a38b7e31..84f080c4d49 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -1,29 +1,111 @@ """The qbittorrent component.""" import logging +from typing import Any from qbittorrent.client import LoginRequired from requests.exceptions import RequestException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import ( + DOMAIN, + SERVICE_GET_ALL_TORRENTS, + SERVICE_GET_TORRENTS, + STATE_ATTR_ALL_TORRENTS, + STATE_ATTR_TORRENTS, + TORRENT_FILTER, +) from .coordinator import QBittorrentDataCoordinator -from .helpers import setup_client +from .helpers import format_torrents, setup_client _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + PLATFORMS = [Platform.SENSOR] +CONF_ENTRY = "entry" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up qBittorrent services.""" + + async def handle_get_torrents(service_call: ServiceCall) -> dict[str, Any] | None: + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(service_call.data[ATTR_DEVICE_ID]) + + if device_entry is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device", + translation_placeholders={ + "device_id": service_call.data[ATTR_DEVICE_ID] + }, + ) + + entry_id = None + + for key, value in device_entry.identifiers: + if key == DOMAIN: + entry_id = value + break + else: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_entry_id", + translation_placeholders={"device_id": entry_id or ""}, + ) + + coordinator: QBittorrentDataCoordinator = hass.data[DOMAIN][entry_id] + items = await coordinator.get_torrents(service_call.data[TORRENT_FILTER]) + info = format_torrents(items) + return { + STATE_ATTR_TORRENTS: info, + } + + hass.services.async_register( + DOMAIN, + SERVICE_GET_TORRENTS, + handle_get_torrents, + supports_response=SupportsResponse.ONLY, + ) + + async def handle_get_all_torrents( + service_call: ServiceCall, + ) -> dict[str, Any] | None: + torrents = {} + + for key, value in hass.data[DOMAIN].items(): + coordinator: QBittorrentDataCoordinator = value + items = await coordinator.get_torrents(service_call.data[TORRENT_FILTER]) + torrents[key] = format_torrents(items) + + return { + STATE_ATTR_ALL_TORRENTS: torrents, + } + + hass.services.async_register( + DOMAIN, + SERVICE_GET_ALL_TORRENTS, + handle_get_all_torrents, + supports_response=SupportsResponse.ONLY, + ) + + return True + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up qBittorrent from a config entry.""" diff --git a/homeassistant/components/qbittorrent/const.py b/homeassistant/components/qbittorrent/const.py index d8fe2c012a3..73e29d06f40 100644 --- a/homeassistant/components/qbittorrent/const.py +++ b/homeassistant/components/qbittorrent/const.py @@ -7,6 +7,13 @@ DOMAIN: Final = "qbittorrent" DEFAULT_NAME = "qBittorrent" DEFAULT_URL = "http://127.0.0.1:8080" +STATE_ATTR_TORRENTS = "torrents" +STATE_ATTR_ALL_TORRENTS = "all_torrents" + STATE_UP_DOWN = "up_down" STATE_SEEDING = "seeding" STATE_DOWNLOADING = "downloading" + +SERVICE_GET_TORRENTS = "get_torrents" +SERVICE_GET_ALL_TORRENTS = "get_all_torrents" +TORRENT_FILTER = "torrent_filter" diff --git a/homeassistant/components/qbittorrent/coordinator.py b/homeassistant/components/qbittorrent/coordinator.py index 32ce4cf9711..850bcf15ca2 100644 --- a/homeassistant/components/qbittorrent/coordinator.py +++ b/homeassistant/components/qbittorrent/coordinator.py @@ -10,7 +10,7 @@ from qbittorrent import Client from qbittorrent.client import LoginRequired from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -19,11 +19,18 @@ _LOGGER = logging.getLogger(__name__) class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): - """Coordinator for updating QBittorrent data.""" + """Coordinator for updating qBittorrent data.""" def __init__(self, hass: HomeAssistant, client: Client) -> None: """Initialize coordinator.""" self.client = client + # self.main_data: dict[str, int] = {} + self.total_torrents: dict[str, int] = {} + self.active_torrents: dict[str, int] = {} + self.inactive_torrents: dict[str, int] = {} + self.paused_torrents: dict[str, int] = {} + self.seeding_torrents: dict[str, int] = {} + self.started_torrents: dict[str, int] = {} super().__init__( hass, @@ -33,7 +40,21 @@ class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) async def _async_update_data(self) -> dict[str, Any]: + """Async method to update QBittorrent data.""" try: return await self.hass.async_add_executor_job(self.client.sync_main_data) except LoginRequired as exc: - raise ConfigEntryError("Invalid authentication") from exc + raise HomeAssistantError(str(exc)) from exc + + async def get_torrents(self, torrent_filter: str) -> list[dict[str, Any]]: + """Async method to get QBittorrent torrents.""" + try: + torrents = await self.hass.async_add_executor_job( + lambda: self.client.torrents(filter=torrent_filter) + ) + except LoginRequired as exc: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="login_error" + ) from exc + + return torrents diff --git a/homeassistant/components/qbittorrent/helpers.py b/homeassistant/components/qbittorrent/helpers.py index b9c29675473..bbe53765f8b 100644 --- a/homeassistant/components/qbittorrent/helpers.py +++ b/homeassistant/components/qbittorrent/helpers.py @@ -1,5 +1,8 @@ """Helper functions for qBittorrent.""" +from datetime import UTC, datetime +from typing import Any + from qbittorrent.client import Client @@ -10,3 +13,48 @@ def setup_client(url: str, username: str, password: str, verify_ssl: bool) -> Cl # Get an arbitrary attribute to test if connection succeeds client.get_alternative_speed_status() return client + + +def seconds_to_hhmmss(seconds) -> str: + """Convert seconds to HH:MM:SS format.""" + if seconds == 8640000: + return "None" + + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" + + +def format_unix_timestamp(timestamp) -> str: + """Format a UNIX timestamp to a human-readable date.""" + dt_object = datetime.fromtimestamp(timestamp, tz=UTC) + return dt_object.isoformat() + + +def format_progress(torrent) -> str: + """Format the progress of a torrent.""" + progress = torrent["progress"] + progress = float(progress) * 100 + return f"{progress:.2f}" + + +def format_torrents(torrents: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Format a list of torrents.""" + value = {} + for torrent in torrents: + value[torrent["name"]] = format_torrent(torrent) + + return value + + +def format_torrent(torrent) -> dict[str, Any]: + """Format a single torrent.""" + value = {} + value["id"] = torrent["hash"] + value["added_date"] = format_unix_timestamp(torrent["added_on"]) + value["percent_done"] = format_progress(torrent) + value["status"] = torrent["state"] + value["eta"] = seconds_to_hhmmss(torrent["eta"]) + value["ratio"] = "{:.2f}".format(float(torrent["ratio"])) + + return value diff --git a/homeassistant/components/qbittorrent/icons.json b/homeassistant/components/qbittorrent/icons.json index bb458c751e1..68fc1020dae 100644 --- a/homeassistant/components/qbittorrent/icons.json +++ b/homeassistant/components/qbittorrent/icons.json @@ -8,5 +8,9 @@ "default": "mdi:cloud-upload" } } + }, + "services": { + "get_torrents": "mdi:file-arrow-up-down-outline", + "get_all_torrents": "mdi:file-arrow-up-down-outline" } } diff --git a/homeassistant/components/qbittorrent/services.yaml b/homeassistant/components/qbittorrent/services.yaml new file mode 100644 index 00000000000..f7fc6b95f64 --- /dev/null +++ b/homeassistant/components/qbittorrent/services.yaml @@ -0,0 +1,35 @@ +get_torrents: + fields: + device_id: + required: true + selector: + device: + integration: qbittorrent + torrent_filter: + required: true + example: "all" + default: "all" + selector: + select: + options: + - "active" + - "inactive" + - "paused" + - "all" + - "seeding" + - "started" +get_all_torrents: + fields: + torrent_filter: + required: true + example: "all" + default: "all" + selector: + select: + options: + - "active" + - "inactive" + - "paused" + - "all" + - "seeding" + - "started" diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 8b20a3354dd..5376e929429 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -48,5 +48,42 @@ "name": "All torrents" } } + }, + "services": { + "get_torrents": { + "name": "Get torrents", + "description": "Gets a list of current torrents", + "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "Which service to grab the list from" + }, + "torrent_filter": { + "name": "Torrent filter", + "description": "What kind of torrents you want to return, such as All or Active." + } + } + }, + "get_all_torrents": { + "name": "Get all torrents", + "description": "Gets a list of current torrents from all instances of qBittorrent", + "fields": { + "torrent_filter": { + "name": "Torrent filter", + "description": "What kind of torrents you want to return, such as All or Active." + } + } + } + }, + "exceptions": { + "invalid_device": { + "message": "No device with id {device_id} was found" + }, + "invalid_entry_id": { + "message": "No entry with id {device_id} was found" + }, + "login_error": { + "message": "A login error occured. Please check you username and password." + } } } diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index c25652ca91e..e0317ab89b5 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -15,7 +15,7 @@ "connectable": false } ], - "codeowners": ["@bdraco", "@skgsergio"], + "codeowners": ["@bdraco"], "config_flow": true, "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/qingping", diff --git a/homeassistant/components/qingping/strings.json b/homeassistant/components/qingping/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/qingping/strings.json +++ b/homeassistant/components/qingping/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 544ef808ca7..38221f89cfd 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -43,19 +43,18 @@ def get_stream_source(guid, client): """Get channel stream source.""" try: resp = client.get_channel_live_stream(guid, protocol="rtsp") - - full_url = resp["resourceUris"] - - protocol = full_url[:7] - auth = f"{client.get_auth_string()}@" - url = full_url[7:] - - return f"{protocol}{auth}{url}" - except QVRResponseError as ex: _LOGGER.error(ex) return None + full_url = resp["resourceUris"] + + protocol = full_url[:7] + auth = f"{client.get_auth_string()}@" + url = full_url[7:] + + return f"{protocol}{auth}{url}" + class QVRProCamera(Camera): """Representation of a QVR Pro camera.""" @@ -91,9 +90,7 @@ class QVRProCamera(Camera): @property def extra_state_attributes(self): """Get the state attributes.""" - attrs = {"qvr_guid": self.guid} - - return attrs + return {"qvr_guid": self.guid} def camera_image( self, width: int | None = None, height: int | None = None diff --git a/homeassistant/components/rabbitair/config_flow.py b/homeassistant/components/rabbitair/config_flow.py index 30dfac93236..6bf48995412 100644 --- a/homeassistant/components/rabbitair/config_flow.py +++ b/homeassistant/components/rabbitair/config_flow.py @@ -8,8 +8,8 @@ from typing import Any from rabbitair import UdpClient 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_ACCESS_TOKEN, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -49,7 +49,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"mac": info.mac} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RabbitAirConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rabbit Air.""" VERSION = 1 @@ -58,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> 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 - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" mac = dr.format_mac(discovery_info.properties["id"]) await self.async_set_unique_id(mac) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index eb7a84867ab..e6248b2c93b 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -2,6 +2,7 @@ from abc import abstractmethod import logging +from typing import Any from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -15,16 +16,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DOMAIN as DOMAIN_RACHIO, + KEY_BATTERY_STATUS, KEY_DEVICE_ID, + KEY_LOW, KEY_RAIN_SENSOR_TRIPPED, + KEY_REPORTED_STATE, + KEY_STATE, KEY_STATUS, KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, STATUS_ONLINE, ) +from .coordinator import RachioUpdateCoordinator from .device import RachioPerson -from .entity import RachioDevice +from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_COLD_REBOOT, SUBTYPE_OFFLINE, @@ -52,6 +58,11 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent for controller in person.controllers: entities.append(RachioControllerOnlineBinarySensor(controller)) entities.append(RachioRainSensor(controller)) + entities.extend( + RachioHoseTimerBattery(valve, base_station.coordinator) + for base_station in person.base_stations + for valve in base_station.coordinator.data.values() + ) return entities @@ -140,3 +151,24 @@ class RachioRainSensor(RachioControllerBinarySensor): self._async_handle_any_update, ) ) + + +class RachioHoseTimerBattery(RachioHoseTimerEntity, BinarySensorEntity): + """Represents a battery sensor for a smart hose timer.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + + def __init__( + self, data: dict[str, Any], coordinator: RachioUpdateCoordinator + ) -> None: + """Initialize a smart hose timer battery sensor.""" + super().__init__(data, coordinator) + self._attr_unique_id = f"{self.id}-battery" + + @callback + def _update_attr(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 = self._static_attrs[KEY_BATTERY_STATUS] == KEY_LOW diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 22c92be2b74..b9b16c0cd87 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -57,6 +57,7 @@ KEY_CONNECTED = "connected" KEY_CURRENT_STATUS = "lastWateringAction" KEY_DETECT_FLOW = "detectFlow" KEY_BATTERY_STATUS = "batteryStatus" +KEY_LOW = "LOW" KEY_REASON = "reason" KEY_DEFAULT_RUNTIME = "defaultRuntimeSeconds" KEY_DURATION_SECONDS = "durationSeconds" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index c018d7e6f86..09f7eaf1b06 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -350,11 +350,9 @@ class RachioBaseStation: def __init__( self, rachio: Rachio, data: dict[str, Any], coordinator: RachioUpdateCoordinator ) -> None: - """Initialize a hose time base station.""" + """Initialize a smart hose timer 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: diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index fc0dc1f1aae..056abe9145b 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -1,10 +1,24 @@ """Adapter to wrap the rachiopy api for home assistant.""" +from abc import abstractmethod +from typing import Any + +from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DEFAULT_NAME, DOMAIN +from .const import ( + DEFAULT_NAME, + DOMAIN, + KEY_CONNECTED, + KEY_ID, + KEY_NAME, + KEY_REPORTED_STATE, + KEY_STATE, +) +from .coordinator import RachioUpdateCoordinator from .device import RachioIro @@ -35,3 +49,45 @@ class RachioDevice(Entity): manufacturer=DEFAULT_NAME, configuration_url="https://app.rach.io", ) + + +class RachioHoseTimerEntity(CoordinatorEntity[RachioUpdateCoordinator]): + """Base class for smart hose timer entities.""" + + _attr_has_entity_name = True + + def __init__( + self, data: dict[str, Any], coordinator: RachioUpdateCoordinator + ) -> None: + """Initialize a Rachio smart hose timer entity.""" + super().__init__(coordinator) + self.id = data[KEY_ID] + self._name = data[KEY_NAME] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.id)}, + model="Smart Hose Timer", + name=self._name, + manufacturer=DEFAULT_NAME, + configuration_url="https://app.rach.io", + ) + self._update_attr() + + @property + def available(self) -> bool: + """Return if the entity is available.""" + return ( + super().available + and self.coordinator.data[self.id][KEY_STATE][KEY_REPORTED_STATE][ + KEY_CONNECTED + ] + ) + + @abstractmethod + def _update_attr(self) -> None: + """Update the state and attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + super()._handle_coordinator_update() diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index fe3d455df3c..1a8dbe42904 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -13,25 +13,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_platform, -) -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers import config_validation as cv, entity_platform 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, @@ -67,9 +59,8 @@ from .const import ( SLOPE_SLIGHT, SLOPE_STEEP, ) -from .coordinator import RachioUpdateCoordinator from .device import RachioPerson -from .entity import RachioDevice +from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_RAIN_DELAY_OFF, SUBTYPE_RAIN_DELAY_ON, @@ -546,39 +537,17 @@ class RachioSchedule(RachioSwitch): ) -class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity): +class RachioValve(RachioHoseTimerEntity, SwitchEntity): """Representation of one smart hose timer valve.""" - def __init__( - self, person, base, data, coordinator: RachioUpdateCoordinator - ) -> None: + _attr_name = None + + def __init__(self, person, base, data, coordinator) -> None: """Initialize a new smart hose valve.""" - super().__init__(coordinator) + super().__init__(data, 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.""" @@ -594,20 +563,19 @@ class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity): 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)) + _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) + _LOGGER.debug("Stopping watering on valve %s", self._name) @callback - def _handle_coordinator_update(self) -> None: + def _update_attr(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/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 70365c2f095..83db2d584d2 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -7,7 +7,6 @@ from dataclasses import dataclass import datetime from functools import cached_property import logging -from typing import TypeVar import aiohttp from pyrainbird.async_client import ( @@ -39,8 +38,6 @@ CONECTION_LIMIT = 1 _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T") - @dataclass class RainbirdDeviceState: diff --git a/homeassistant/components/rapt_ble/strings.json b/homeassistant/components/rapt_ble/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/rapt_ble/strings.json +++ b/homeassistant/components/rapt_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index de75207389f..26b9f471b9e 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers.integration_platform import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.event_type import EventType from . import entity_registry, websocket_api from .const import ( # noqa: F401 @@ -146,7 +147,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass_config_path=hass.config.path(DEFAULT_DB_FILE) ) exclude = conf[CONF_EXCLUDE] - exclude_event_types: set[str] = set(exclude.get(CONF_EVENT_TYPES, [])) + exclude_event_types: set[EventType[Any] | str] = set( + exclude.get(CONF_EVENT_TYPES, []) + ) if EVENT_STATE_CHANGED in exclude_event_types: _LOGGER.error("State change events cannot be excluded, use a filter instead") exclude_event_types.remove(EVENT_STATE_CHANGED) diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index aa2fc1bb8cb..41be13312d0 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -55,8 +55,8 @@ def validate_table_schema_supports_utf8( schema_errors = _validate_table_schema_supports_utf8( instance, table_object, columns ) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Error when validating DB schema: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema") _log_schema_errors(table_object, schema_errors) return schema_errors @@ -76,8 +76,8 @@ def validate_table_schema_has_correct_collation( schema_errors = _validate_table_schema_has_correct_collation( instance, table_object ) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Error when validating DB schema: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema") _log_schema_errors(table_object, schema_errors) return schema_errors @@ -159,8 +159,8 @@ def validate_db_schema_precision( return schema_errors try: schema_errors = _validate_db_schema_precision(instance, table_object) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Error when validating DB schema: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema") _log_schema_errors(table_object, schema_errors) return schema_errors diff --git a/homeassistant/components/recorder/auto_repairs/states/__init__.py b/homeassistant/components/recorder/auto_repairs/states/__init__.py new file mode 100644 index 00000000000..0429e0cab12 --- /dev/null +++ b/homeassistant/components/recorder/auto_repairs/states/__init__.py @@ -0,0 +1 @@ +"""States repairs for Recorder.""" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 0e404ce4da0..92d9baed771 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -30,7 +30,13 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, MATCH_ALL, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + callback, +) from homeassistant.helpers.event import ( async_track_time_change, async_track_time_interval, @@ -40,6 +46,7 @@ from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util from homeassistant.util.enum import try_parse_enum +from homeassistant.util.event_type import EventType from . import migration, statistics from .const import ( @@ -173,7 +180,7 @@ class Recorder(threading.Thread): db_max_retries: int, db_retry_wait: int, entity_filter: Callable[[str], bool], - exclude_event_types: set[str], + exclude_event_types: set[EventType[Any] | str], ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -332,7 +339,6 @@ class Recorder(threading.Thread): self._event_listener = self.hass.bus.async_listen( MATCH_ALL, _event_listener, - run_immediately=True, ) self._queue_watcher = async_track_time_interval( self.hass, @@ -477,12 +483,8 @@ 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, run_immediately=True - ) - bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_shutdown, run_immediately=True - ) + bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, self._async_close) + bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_shutdown) async_at_started(self.hass, self._async_hass_started) @callback @@ -866,12 +868,12 @@ class Recorder(threading.Thread): self._guarded_process_one_task_or_event_or_recover(queue_.get()) def _pre_process_startup_events( - self, startup_task_or_events: list[RecorderTask | Event] + self, startup_task_or_events: list[RecorderTask | Event[Any]] ) -> None: """Pre process startup events.""" # Prime all the state_attributes and event_data caches # before we start processing events - state_change_events: list[Event] = [] + state_change_events: list[Event[EventStateChangedData]] = [] non_state_change_events: list[Event] = [] for task_or_event in startup_task_or_events: @@ -898,8 +900,8 @@ class Recorder(threading.Thread): _LOGGER.debug("Processing task: %s", task) try: self._process_one_task_or_event_or_recover(task) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while processing event %s: %s", task, err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error while processing event %s", task) def _process_one_task_or_event_or_recover(self, task: RecorderTask | Event) -> None: """Process a task or event, reconnect, or recover a malformed database.""" @@ -921,11 +923,9 @@ class Recorder(threading.Thread): except exc.DatabaseError as err: if self._handle_database_error(err): return - _LOGGER.exception( - "Unhandled database error while processing task %s: %s", task, err - ) - except SQLAlchemyError as err: - _LOGGER.exception("SQLAlchemyError error processing task %s: %s", task, err) + _LOGGER.exception("Unhandled database error while processing task %s", task) + except SQLAlchemyError: + _LOGGER.exception("SQLAlchemyError error processing task %s", task) # Reset the session if an SQLAlchemyError (including DatabaseError) # happens to rollback and recover @@ -941,10 +941,9 @@ class Recorder(threading.Thread): return migration.initialize_database(self.get_session) except UnsupportedDialect: break - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error during connection setup: %s (retrying in %s seconds)", - err, + "Error during connection setup: (retrying in %s seconds)", self.db_retry_wait, ) tries += 1 @@ -1026,7 +1025,7 @@ class Recorder(threading.Thread): self.backlog, ) - def _process_one_event(self, event: Event) -> None: + def _process_one_event(self, event: Event[Any]) -> None: if not self.enabled: return if event.event_type == EVENT_STATE_CHANGED: @@ -1083,7 +1082,9 @@ class Recorder(threading.Thread): self._add_to_session(session, dbevent) - def _process_state_changed_event_into_session(self, event: Event) -> None: + def _process_state_changed_event_into_session( + self, event: Event[EventStateChangedData] + ) -> None: """Process a state_changed event into the session.""" state_attributes_manager = self.state_attributes_manager states_meta_manager = self.states_meta_manager @@ -1182,7 +1183,6 @@ class Recorder(threading.Thread): while tries <= self.db_max_retries: try: self._commit_event_session() - return except (exc.InternalError, exc.OperationalError) as err: _LOGGER.error( "%s: Error executing query: %s. (retrying in %s seconds)", @@ -1195,6 +1195,8 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) + else: + return def _commit_event_session(self) -> None: assert self.event_session is not None @@ -1262,10 +1264,8 @@ class Recorder(threading.Thread): try: self.event_session.rollback() self.event_session.close() - except SQLAlchemyError as err: - _LOGGER.exception( - "Error while rolling back and closing the event session: %s", err - ) + except SQLAlchemyError: + _LOGGER.exception("Error while rolling back and closing the event session") def _reopen_event_session(self) -> None: """Rollback the event session and reopen it after a failure.""" @@ -1473,8 +1473,8 @@ class Recorder(threading.Thread): self.recorder_runs_manager.end(self.event_session) try: self._commit_event_session_or_retry() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error saving the event session during shutdown: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error saving the event session during shutdown") self.event_session.close() self.recorder_runs_manager.clear() diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index eac743c3d75..186b873047b 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -40,7 +40,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, ) -from homeassistant.core import Context, Event, EventOrigin, State +from homeassistant.core import Context, Event, EventOrigin, EventStateChangedData, 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 ( @@ -478,10 +478,10 @@ class States(Base): return date_time.isoformat(sep=" ", timespec="seconds") @staticmethod - def from_event(event: Event) -> States: + def from_event(event: Event[EventStateChangedData]) -> States: """Create object from a state_changed event.""" entity_id = event.data["entity_id"] - state: State | None = event.data.get("new_state") + state = event.data["new_state"] dbstate = States( entity_id=entity_id, attributes=None, @@ -576,13 +576,12 @@ class StateAttributes(Base): @staticmethod def shared_attrs_bytes_from_event( - event: Event, + event: Event[EventStateChangedData], 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: + if (state := event.data["new_state"]) is None: return b"{}" if state_info := state.state_info: exclude_attrs = { diff --git a/homeassistant/components/recorder/entity_registry.py b/homeassistant/components/recorder/entity_registry.py index 5bf1856316a..07f8f2f88de 100644 --- a/homeassistant/components/recorder/entity_registry.py +++ b/homeassistant/components/recorder/entity_registry.py @@ -1,8 +1,7 @@ """Recorder entity registry helper.""" -from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -19,10 +18,14 @@ def async_setup(hass: HomeAssistant) -> None: """Set up the entity hooks.""" @callback - def _async_entity_id_changed(event: Event) -> None: + def _async_entity_id_changed( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: instance = get_instance(hass) - old_entity_id: str = event.data["old_entity_id"] - new_entity_id: str = event.data["entity_id"] + if TYPE_CHECKING: + assert event.data["action"] == "update" and "old_entity_id" in event.data + old_entity_id = event.data["old_entity_id"] + new_entity_id = event.data["entity_id"] instance.async_update_statistics_metadata( old_entity_id, new_statistic_id=new_entity_id ) @@ -31,7 +34,9 @@ def async_setup(hass: HomeAssistant) -> None: ) @callback - def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool: + def entity_registry_changed_filter( + event_data: er.EventEntityRegistryUpdatedData, + ) -> bool: """Handle entity_id changed filter.""" return event_data["action"] == "update" and "old_entity_id" in event_data @@ -42,7 +47,6 @@ def async_setup(hass: HomeAssistant) -> None: er.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_id_changed, event_filter=entity_registry_changed_filter, - run_immediately=True, ) async_at_start(hass, _setup_entity_registry_event_handler) diff --git a/homeassistant/components/recorder/history/__init__.py b/homeassistant/components/recorder/history/__init__.py index 05452fdac47..de7002eb6a4 100644 --- a/homeassistant/components/recorder/history/__init__.py +++ b/homeassistant/components/recorder/history/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import MutableMapping from datetime import datetime from typing import Any @@ -43,7 +42,7 @@ def get_full_significant_states_with_session( include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return a dict of significant states during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -68,7 +67,7 @@ def get_full_significant_states_with_session( def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return the last number_of_states.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -92,7 +91,7 @@ def get_significant_states( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -128,7 +127,7 @@ def get_significant_states_with_session( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -162,7 +161,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return a list of states that changed during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index ad9505e1af2..8ee3cd30316 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable, Iterator, MutableMapping +from collections.abc import Callable, Iterable, Iterator from datetime import datetime from itertools import groupby from operator import attrgetter @@ -209,7 +209,7 @@ def get_significant_states( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass, read_only=True) as session: return get_significant_states_with_session( @@ -317,7 +317,7 @@ def get_significant_states_with_session( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return states changes during UTC period start_time - end_time. entity_ids is an optional iterable of entities to include in the results. @@ -365,14 +365,14 @@ def get_full_significant_states_with_session( include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Variant of get_significant_states_with_session. Difference with get_significant_states_with_session is that it does not return minimal responses. """ return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], get_significant_states_with_session( hass=hass, session=session, @@ -454,7 +454,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" if not entity_id: raise ValueError("entity_id must be provided") @@ -471,7 +471,7 @@ def state_changes_during_period( ) states = execute_stmt_lambda_element(session, stmt, None, end_time) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( hass, session, @@ -522,7 +522,7 @@ def _get_last_state_changes_stmt( def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return the last number_of_states.""" entity_id_lower = entity_id.lower() entity_ids = [entity_id_lower] @@ -533,7 +533,7 @@ def get_last_state_changes( ) states = list(execute_stmt_lambda_element(session, stmt)) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( hass, session, @@ -693,7 +693,7 @@ def _sorted_states_to_dict( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 5fd4f415e02..96347a1f57b 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Iterator, MutableMapping +from collections.abc import Callable, Iterable, Iterator from datetime import datetime from itertools import groupby from operator import itemgetter @@ -117,7 +117,7 @@ def get_significant_states( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass, read_only=True) as session: return get_significant_states_with_session( @@ -174,8 +174,7 @@ def _significant_states_stmt( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if not include_start_time_state or not run_start_ts: - stmt = stmt.order_by(States.metadata_id, States.last_updated_ts) - return stmt + return stmt.order_by(States.metadata_id, States.last_updated_ts) unioned_subquery = union_all( _select_from_subquery( _get_start_time_state_stmt( @@ -214,7 +213,7 @@ def get_significant_states_with_session( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return states changes during UTC period start_time - end_time. entity_ids is an optional iterable of entities to include in the results. @@ -297,14 +296,14 @@ def get_full_significant_states_with_session( include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Variant of get_significant_states_with_session. Difference with get_significant_states_with_session is that it does not return minimal responses. """ return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], get_significant_states_with_session( hass=hass, session=session, @@ -391,7 +390,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, list[State]]: +) -> dict[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 @@ -439,7 +438,7 @@ def state_changes_during_period( ], ) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( execute_stmt_lambda_element( session, stmt, None, end_time, orm_rows=False @@ -505,7 +504,7 @@ def _get_last_state_changes_multiple_stmt( def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return the last number_of_states.""" has_last_reported = ( recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION @@ -540,7 +539,7 @@ def get_last_state_changes( ) states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False)) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( reversed(states), None, @@ -681,7 +680,7 @@ def _sorted_states_to_dict( compressed_state_format: bool = False, descending: bool = False, no_attributes: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index fc2e6ec2b3f..8724846def5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -183,8 +183,8 @@ def get_schema_version(session_maker: Callable[[], Session]) -> int | None: try: 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) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when determining DB schema version") return None @@ -341,10 +341,10 @@ def _execute_or_collect_error( with session_scope(session=session_maker()) as session: try: session.connection().execute(text(query)) - return True except SQLAlchemyError as err: errors.append(str(err)) - return False + return False + return True def _drop_index( @@ -439,11 +439,12 @@ def _add_columns( ) ) ) - return except (InternalError, OperationalError, ProgrammingError): # Some engines support adding all columns at once, # this error is when they don't _LOGGER.info("Unable to use quick column add. Adding 1 by 1") + else: + return for column_def in columns_def: with session_scope(session=session_maker()) as session: @@ -491,7 +492,7 @@ def _modify_columns( if engine.dialect.name == SupportedDialect.POSTGRESQL: columns_def = [ "ALTER {column} TYPE {type}".format( - **dict(zip(["column", "type"], col_def.split(" ", 1))) + **dict(zip(["column", "type"], col_def.split(" ", 1), strict=False)) ) for col_def in columns_def ] @@ -510,9 +511,10 @@ def _modify_columns( ) ) ) - return except (InternalError, OperationalError): _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") + else: + return for column_def in columns_def: with session_scope(session=session_maker()) as session: @@ -1786,8 +1788,8 @@ def initialize_database(session_maker: Callable[[], Session]) -> bool: 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) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when initialise database") return False diff --git a/homeassistant/components/recorder/models/context.py b/homeassistant/components/recorder/models/context.py index c09ee366b84..0d81bab879a 100644 --- a/homeassistant/components/recorder/models/context.py +++ b/homeassistant/components/recorder/models/context.py @@ -18,8 +18,8 @@ def ulid_to_bytes_or_none(ulid: str | None) -> bytes | None: return None try: return ulid_to_bytes(ulid) - except ValueError as ex: - _LOGGER.exception("Error converting ulid %s to bytes: %s", ulid, ex) + except ValueError: + _LOGGER.exception("Error converting ulid %s to bytes", ulid) return None @@ -29,8 +29,8 @@ def bytes_to_ulid_or_none(_bytes: bytes | None) -> str | None: return None try: return bytes_to_ulid(_bytes) - except ValueError as ex: - _LOGGER.exception("Error converting bytes %s to ulid: %s", _bytes, ex) + except ValueError: + _LOGGER.exception("Error converting bytes %s to ulid", _bytes) return None diff --git a/homeassistant/components/recorder/models/event.py b/homeassistant/components/recorder/models/event.py index 379a6fddb1d..4e5030bfde7 100644 --- a/homeassistant/components/recorder/models/event.py +++ b/homeassistant/components/recorder/models/event.py @@ -2,9 +2,13 @@ from __future__ import annotations +from typing import Any + +from homeassistant.util.event_type import EventType + def extract_event_type_ids( - event_type_to_event_type_id: dict[str, int | None], + event_type_to_event_type_id: dict[EventType[Any] | str, int | None], ) -> list[int]: """Extract event_type ids from event_type_to_event_type_id.""" return [ diff --git a/homeassistant/components/recorder/models/state.py b/homeassistant/components/recorder/models/state.py index e1f23f32118..ca70b856d76 100644 --- a/homeassistant/components/recorder/models/state.py +++ b/homeassistant/components/recorder/models/state.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime +from functools import cached_property import logging from typing import TYPE_CHECKING, Any @@ -19,11 +20,6 @@ import homeassistant.util.dt as dt_util from .state_attributes import decode_attributes_from_source -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) EMPTY_CONTEXT = Context(id=None) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 27bc313b162..ec7aa5bdcb6 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -14,7 +14,7 @@ from sqlalchemy.pool import ( ) from homeassistant.helpers.frame import report -from homeassistant.util.async_ import check_loop +from homeassistant.util.loop import check_loop from .const import DB_WORKER_PREFIX diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index f42bae00abe..c78f8a4a89d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -694,7 +694,7 @@ def _purge_filtered_states( ) if not to_purge: return True - state_ids, attributes_ids, event_ids = zip(*to_purge) + state_ids, attributes_ids, event_ids = zip(*to_purge, strict=False) filtered_event_ids = {id_ for id_ in event_ids if id_ is not None} _LOGGER.debug( "Selected %s state_ids to remove that should be filtered", len(state_ids) @@ -735,7 +735,7 @@ def _purge_filtered_events( ) if not to_purge: return True - event_ids, data_ids = zip(*to_purge) + event_ids, data_ids = zip(*to_purge, strict=False) event_ids_set = set(event_ids) _LOGGER.debug( "Selected %s event_ids to remove that should be filtered", len(event_ids_set) diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py index b4d719a9481..2be02fe8091 100644 --- a/homeassistant/components/recorder/services.py +++ b/homeassistant/components/recorder/services.py @@ -7,6 +7,7 @@ from typing import cast import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter @@ -36,15 +37,28 @@ SERVICE_PURGE_SCHEMA = vol.Schema( ATTR_DOMAINS = "domains" ATTR_ENTITY_GLOBS = "entity_globs" -SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( - cv.ensure_list, [cv.string] +SERVICE_PURGE_ENTITIES_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Optional(ATTR_DOMAINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_KEEP_DAYS, default=0): cv.positive_int, + } + ), + vol.Any( + vol.Schema({vol.Required(ATTR_ENTITY_ID): vol.IsTrue()}, extra=vol.ALLOW_EXTRA), + vol.Schema({vol.Required(ATTR_DOMAINS): vol.IsTrue()}, extra=vol.ALLOW_EXTRA), + vol.Schema( + {vol.Required(ATTR_ENTITY_GLOBS): vol.IsTrue()}, extra=vol.ALLOW_EXTRA ), - vol.Optional(ATTR_KEEP_DAYS, default=0): cv.positive_int, - } -).extend(cv.ENTITY_SERVICE_FIELDS) + msg="At least one of entity_id, domains, or entity_globs must have a value", + ), +) SERVICE_ENABLE_SCHEMA = vol.Schema({}) SERVICE_DISABLE_SCHEMA = vol.Schema({}) diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index b74dcc2a494..7d7b926548c 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -20,20 +20,21 @@ purge: boolean: purge_entities: - target: - entity: {} fields: + entity_id: + required: false + selector: + entity: + multiple: true domains: example: "sun" required: false - default: [] selector: object: entity_globs: example: "domain*.object_id*" required: false - default: [] selector: object: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f840fdbd7b6..41cf4e22b53 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -349,8 +349,7 @@ def get_start_time() -> datetime: now = dt_util.utcnow() current_period_minutes = now.minute - now.minute % 5 current_period = now.replace(minute=current_period_minutes, second=0, microsecond=0) - last_period = current_period - timedelta(minutes=5) - return last_period + return current_period - timedelta(minutes=5) def _compile_hourly_statistics_summary_mean_stmt( @@ -641,7 +640,6 @@ def _insert_statistics( try: stat = table.from_stats(metadata_id, statistic) session.add(stat) - return stat except SQLAlchemyError: _LOGGER.exception( "Unexpected exception when inserting statistics %s:%s ", @@ -649,6 +647,7 @@ def _insert_statistics( statistic, ) return None + return stat def _update_statistics( @@ -685,7 +684,7 @@ def get_metadata_with_session( session: Session, *, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data. @@ -706,7 +705,7 @@ def get_metadata( hass: HomeAssistant, *, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Return metadata for statistic_ids.""" @@ -754,7 +753,7 @@ def update_statistics_metadata( async def async_list_statistic_ids( hass: HomeAssistant, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, ) -> list[dict]: """Return all statistic_ids (or filtered one) and unit of measurement. @@ -824,7 +823,7 @@ def _flatten_list_statistic_ids_metadata_result( def list_statistic_ids( hass: HomeAssistant, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, ) -> list[dict]: """Return all statistic_ids (or filtered one) and unit of measurement. diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 74b248354d7..bf5d95ae1fc 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -41,6 +41,10 @@ "name": "Purge entities", "description": "Starts a purge task to remove the data related to specific entities from your database.", "fields": { + "entity_id": { + "name": "Entities to remove", + "description": "List of entities for which the data is to be removed from the recorder database." + }, "domains": { "name": "Domains to remove", "description": "List of domains for which the data needs to be removed from the recorder database." diff --git a/homeassistant/components/recorder/table_managers/__init__.py b/homeassistant/components/recorder/table_managers/__init__.py index 9a0945dc4d9..c064987ddcb 100644 --- a/homeassistant/components/recorder/table_managers/__init__.py +++ b/homeassistant/components/recorder/table_managers/__init__.py @@ -1,9 +1,11 @@ """Managers for each table.""" -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from lru import LRU +from homeassistant.util.event_type import EventType + if TYPE_CHECKING: from ..core import Recorder @@ -13,7 +15,7 @@ _DataT = TypeVar("_DataT") class BaseTableManager(Generic[_DataT]): """Base class for table managers.""" - _id_map: "LRU[str, int]" + _id_map: "LRU[EventType[Any] | str, int]" def __init__(self, recorder: "Recorder") -> None: """Initialize the table manager. @@ -24,7 +26,7 @@ class BaseTableManager(Generic[_DataT]): """ self.active = False self.recorder = recorder - self._pending: dict[str, _DataT] = {} + self._pending: dict[EventType[Any] | str, _DataT] = {} def get_from_cache(self, data: str) -> int | None: """Resolve data to the id without accessing the underlying database. @@ -34,7 +36,7 @@ class BaseTableManager(Generic[_DataT]): """ return self._id_map.get(data) - def get_pending(self, shared_data: str) -> _DataT | None: + def get_pending(self, shared_data: EventType[Any] | str) -> _DataT | None: """Get pending data that have not be assigned ids yet. This call is not thread-safe and must be called from the diff --git a/homeassistant/components/recorder/table_managers/event_types.py b/homeassistant/components/recorder/table_managers/event_types.py index 94ceab7bf68..73401e8df56 100644 --- a/homeassistant/components/recorder/table_managers/event_types.py +++ b/homeassistant/components/recorder/table_managers/event_types.py @@ -3,12 +3,13 @@ from __future__ import annotations from collections.abc import Iterable -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast from lru import LRU from sqlalchemy.orm.session import Session from homeassistant.core import Event +from homeassistant.util.event_type import EventType from ..db_schema import EventTypes from ..queries import find_event_type_ids @@ -29,7 +30,9 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): def __init__(self, recorder: Recorder) -> None: """Initialize the event type manager.""" super().__init__(recorder, CACHE_SIZE) - self._non_existent_event_types: LRU[str, None] = LRU(CACHE_SIZE) + self._non_existent_event_types: LRU[EventType[Any] | str, None] = LRU( + CACHE_SIZE + ) def load(self, events: list[Event], session: Session) -> None: """Load the event_type to event_type_ids mapping into memory. @@ -44,7 +47,10 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): ) def get( - self, event_type: str, session: Session, from_recorder: bool = False + self, + event_type: EventType[Any] | str, + session: Session, + from_recorder: bool = False, ) -> int | None: """Resolve event_type to the event_type_id. @@ -54,16 +60,19 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): return self.get_many((event_type,), session)[event_type] def get_many( - self, event_types: Iterable[str], session: Session, from_recorder: bool = False - ) -> dict[str, int | None]: + self, + event_types: Iterable[EventType[Any] | str], + session: Session, + from_recorder: bool = False, + ) -> dict[EventType[Any] | str, int | None]: """Resolve event_types to event_type_ids. This call is not thread-safe and must be called from the recorder thread. """ - results: dict[str, int | None] = {} - missing: list[str] = [] - non_existent: list[str] = [] + results: dict[EventType[Any] | str, int | None] = {} + missing: list[EventType[Any] | str] = [] + non_existent: list[EventType[Any] | str] = [] for event_type in event_types: if (event_type_id := self._id_map.get(event_type)) is None: @@ -123,7 +132,7 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): self.clear_non_existent(event_type) self._pending.clear() - def clear_non_existent(self, event_type: str) -> None: + def clear_non_existent(self, event_type: EventType[Any] | str) -> None: """Clear a non-existent event type from the cache. This call is not thread-safe and must be called from the diff --git a/homeassistant/components/recorder/table_managers/state_attributes.py b/homeassistant/components/recorder/table_managers/state_attributes.py index e2fb9153be8..ec975d310e9 100644 --- a/homeassistant/components/recorder/table_managers/state_attributes.py +++ b/homeassistant/components/recorder/table_managers/state_attributes.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session -from homeassistant.core import Event +from homeassistant.core import Event, EventStateChangedData from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS from ..db_schema import StateAttributes @@ -38,7 +38,7 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): super().__init__(recorder, CACHE_SIZE) self.active = True # always active - def serialize_from_event(self, event: Event) -> bytes | None: + def serialize_from_event(self, event: Event[EventStateChangedData]) -> bytes | None: """Serialize event data.""" try: return StateAttributes.shared_attrs_bytes_from_event( @@ -47,12 +47,14 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning( "State is not JSON serializable: %s: %s", - event.data.get("new_state"), + event.data["new_state"], ex, ) return None - def load(self, events: list[Event], session: Session) -> None: + def load( + self, events: list[Event[EventStateChangedData]], session: Session + ) -> None: """Load the shared_attrs to attributes_ids mapping into memory from events. This call is not thread-safe and must be called from the diff --git a/homeassistant/components/recorder/table_managers/states_meta.py b/homeassistant/components/recorder/table_managers/states_meta.py index ebc1dab45f3..2c73dcf3a54 100644 --- a/homeassistant/components/recorder/table_managers/states_meta.py +++ b/homeassistant/components/recorder/table_managers/states_meta.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session -from homeassistant.core import Event +from homeassistant.core import Event, EventStateChangedData from ..db_schema import StatesMeta from ..queries import find_all_states_metadata_ids, find_states_metadata_ids @@ -28,7 +28,9 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]): self._did_first_load = False super().__init__(recorder, CACHE_SIZE) - def load(self, events: list[Event], session: Session) -> None: + def load( + self, events: list[Event[EventStateChangedData]], session: Session + ) -> None: """Load the entity_id to metadata_id mapping into memory. This call is not thread-safe and must be called from the @@ -37,9 +39,9 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]): self._did_first_load = True self.get_many( { - event.data["new_state"].entity_id + new_state.entity_id for event in events - if event.data.get("new_state") is not None + if (new_state := event.data["new_state"]) is not None }, session, True, diff --git a/homeassistant/components/recorder/table_managers/statistics_meta.py b/homeassistant/components/recorder/table_managers/statistics_meta.py index 32e989b0e3d..9b33eff0c9b 100644 --- a/homeassistant/components/recorder/table_managers/statistics_meta.py +++ b/homeassistant/components/recorder/table_managers/statistics_meta.py @@ -36,7 +36,7 @@ QUERY_STATISTIC_META = ( def _generate_get_metadata_stmt( statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> StatementLambdaElement: """Generate a statement to fetch metadata.""" @@ -88,7 +88,7 @@ class StatisticsMetaManager: self, session: Session, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data and process it into results and/or cache.""" @@ -202,7 +202,7 @@ class StatisticsMetaManager: self, session: Session, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data. diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 1b81d7a983f..2d980c849e5 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -12,6 +12,7 @@ import threading from typing import TYPE_CHECKING, Any from homeassistant.helpers.typing import UndefinedType +from homeassistant.util.event_type import EventType from . import entity_registry, purge, statistics from .const import DOMAIN @@ -459,7 +460,7 @@ class EventIdMigrationTask(RecorderTask): class RefreshEventTypesTask(RecorderTask): """An object to insert into the recorder queue to refresh event types.""" - event_types: list[str] + event_types: list[EventType[Any] | str] def run(self, instance: Recorder) -> None: """Refresh event types.""" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 770dc91353c..ad96833b1d7 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -143,7 +143,7 @@ def session_scope( need_rollback = True session.commit() except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error executing query: %s", err) + _LOGGER.exception("Error executing query") if need_rollback: session.rollback() if not exception_filter or not exception_filter(err): @@ -192,13 +192,14 @@ def execute( elapsed, ) - return result except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: raise time.sleep(QUERY_RETRY_WAIT) + else: + return result # Unreachable raise RuntimeError # pragma: no cover @@ -685,7 +686,6 @@ def database_job_retry_wrapper( for attempt in range(attempts): try: job(instance, *args, **kwargs) - return except OperationalError as err: if attempt == attempts - 1 or not _is_retryable_error( instance, err @@ -697,6 +697,8 @@ def database_job_retry_wrapper( ) time.sleep(instance.db_retry_wait) # Failed with retryable error + else: + return return wrapper diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 79104485e19..58c362df62e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -235,7 +235,7 @@ async def ws_get_statistics_during_period( def _ws_get_list_statistic_ids( hass: HomeAssistant, msg_id: int, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, ) -> bytes: """Fetch a list of available statistic_id and convert them to JSON. diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 2b88c51e936..88813e4a70c 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -6,8 +6,9 @@ from collections.abc import Iterable from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -37,12 +38,6 @@ from homeassistant.helpers.entity_component import EntityComponent 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 - - _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" diff --git a/homeassistant/components/renault/coordinator.py b/homeassistant/components/renault/coordinator.py index f77a38f2505..d7aed6e3560 100644 --- a/homeassistant/components/renault/coordinator.py +++ b/homeassistant/components/renault/coordinator.py @@ -55,8 +55,6 @@ class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): try: async with _PARALLEL_SEMAPHORE: data = await self.update_method() - self._has_already_worked = True - return data except AccessDeniedException as err: # This can mean both a temporary error or a permanent error. If it has @@ -76,6 +74,9 @@ class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): # Other Renault errors. raise UpdateFailed(f"Error communicating with API: {err}") from err + self._has_already_worked = True + return data + async def async_config_entry_first_refresh(self) -> None: """Refresh data for the first time when a config entry is setup. diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 44750cdeb3c..4f5487a6a04 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -351,7 +351,7 @@ class ReolinkHost: await self._api.subscribe(sub_type=SubType.long_poll) except NotSupportedError as err: if initial: - raise err + raise # make sure the long_poll_task is always created to try again later if not self._lost_subscription: self._lost_subscription = True @@ -547,12 +547,12 @@ class ReolinkHost: self._long_poll_error = True await asyncio.sleep(LONG_POLL_ERROR_COOLDOWN) continue - except Exception as ex: + except Exception: _LOGGER.exception( - "Unexpected exception while requesting ONVIF pull point: %s", ex + "Unexpected exception while requesting ONVIF pull point" ) await self._api.unsubscribe(sub_type=SubType.long_poll) - raise ex + raise self._long_poll_error = False @@ -652,11 +652,9 @@ class ReolinkHost: message = data.decode("utf-8") channels = await self._api.ONVIF_event_callback(message) - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error processing ONVIF event for Reolink %s: %s", - self._api.nvr_name, - ex, + "Error processing ONVIF event for Reolink %s", self._api.nvr_name ) return diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 2282289bdbc..ec81893d846 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -28,7 +28,7 @@ "api_error": "API error occurred", "cannot_connect": "Failed to connect, check the IP address of the camera", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "not_admin": "User needs to be admin, user \"{username}\" has authorisation level \"{userlevel}\"", "unknown": "[%key:common::config_flow::error::unknown%]", "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" }, diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 1c33b4592df..b7cdee2e039 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -45,6 +45,7 @@ from homeassistant.util.async_ import create_eager_task from .const import ( CONF_ENCODING, + CONF_PAYLOAD_TEMPLATE, CONF_SSL_CIPHER_LIST, COORDINATOR, DEFAULT_SSL_CIPHER_LIST, @@ -108,8 +109,11 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool for rest_idx, conf in enumerate(rest_config): scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE) + payload_template: template.Template | None = conf.get(CONF_PAYLOAD_TEMPLATE) rest = create_rest_data_from_config(hass, conf) - coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval) + coordinator = _rest_coordinator( + hass, rest, resource_template, payload_template, scan_interval + ) refresh_coroutines.append(coordinator.async_refresh()) hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) @@ -156,16 +160,20 @@ def _rest_coordinator( hass: HomeAssistant, rest: RestData, resource_template: template.Template | None, + payload_template: template.Template | None, update_interval: timedelta, ) -> DataUpdateCoordinator[None]: """Wrap a DataUpdateCoordinator around the rest object.""" - if resource_template: + if resource_template or payload_template: - async def _async_refresh_with_resource_template() -> None: - rest.set_url(resource_template.async_render(parse_result=False)) + async def _async_refresh_with_templates() -> None: + if resource_template: + rest.set_url(resource_template.async_render(parse_result=False)) + if payload_template: + rest.set_payload(payload_template.async_render(parse_result=False)) await rest.async_update() - update_method = _async_refresh_with_resource_template + update_method = _async_refresh_with_templates else: update_method = rest.async_update @@ -184,6 +192,7 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res resource_template: template.Template | None = config.get(CONF_RESOURCE_TEMPLATE) method: str = config[CONF_METHOD] payload: str | None = config.get(CONF_PAYLOAD) + payload_template: template.Template | None = config.get(CONF_PAYLOAD_TEMPLATE) verify_ssl: bool = config[CONF_VERIFY_SSL] ssl_cipher_list: str = config.get(CONF_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST) username: str | None = config.get(CONF_USERNAME) @@ -196,6 +205,10 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res resource_template.hass = hass resource = resource_template.async_render(parse_result=False) + if payload_template is not None: + payload_template.hass = hass + payload = payload_template.async_render(parse_result=False) + if not resource: raise HomeAssistantError("Resource not set for RestData") diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py index 8fb08f766fa..d10b3f3f74e 100644 --- a/homeassistant/components/rest/const.py +++ b/homeassistant/components/rest/const.py @@ -33,3 +33,5 @@ XML_MIME_TYPES = ( "application/xml", "text/xml", ) + +CONF_PAYLOAD_TEMPLATE = "payload_template" diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 06be7a4f6ff..4c9667e7651 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -56,6 +56,10 @@ class RestData: self.last_exception: Exception | None = None self.headers: httpx.Headers | None = None + def set_payload(self, payload: str) -> None: + """Set request data.""" + self._request_data = payload + @property def url(self) -> str: """Get url.""" diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d6011a43efd..f7fd8a36113 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -38,6 +38,7 @@ from .const import ( CONF_ENCODING, CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, + CONF_PAYLOAD_TEMPLATE, CONF_SSL_CIPHER_LIST, DEFAULT_ENCODING, DEFAULT_FORCE_UPDATE, @@ -60,7 +61,8 @@ RESOURCE_SCHEMA = { vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PAYLOAD): cv.string, + vol.Exclusive(CONF_PAYLOAD, CONF_PAYLOAD): cv.string, + vol.Exclusive(CONF_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional( CONF_SSL_CIPHER_LIST, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index f0da6366cfc..99aadce6620 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -219,8 +219,8 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): req = await self.get_device_state(self.hass) except (TimeoutError, httpx.TimeoutException): _LOGGER.exception("Timed out while fetching data") - except httpx.RequestError as err: - _LOGGER.exception("Error while fetching data: %s", err) + except httpx.RequestError: + _LOGGER.exception("Error while fetching data") if req: self._process_manual_data(req.text) diff --git a/homeassistant/components/rest_command/strings.json b/homeassistant/components/rest_command/strings.json index 2d658ad8b20..fd0b26f6499 100644 --- a/homeassistant/components/rest_command/strings.json +++ b/homeassistant/components/rest_command/strings.json @@ -7,13 +7,13 @@ }, "exceptions": { "timeout": { - "message": "Timeout when calling resource '{request_url}'" + "message": "Timeout when calling resource \"{request_url}\"" }, "client_error": { - "message": "Client error occurred when calling resource '{request_url}'" + "message": "Client error occurred when calling resource \"{request_url}\"" }, "decoding_error": { - "message": "The response of '{request_url}' 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 74b7d4aa4c0..e5d5e97fa84 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -209,7 +209,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: TMP_ENTITY.format(event_id) ) hass.async_create_task( - hass.data[DATA_DEVICE_REGISTER][event_type](event) + hass.data[DATA_DEVICE_REGISTER][event_type](event), + eager_start=False, ) else: _LOGGER.debug("device_id not known and automatic add disabled") @@ -257,7 +258,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # If HA is not stopping, initiate new connection if hass.state is not CoreState.stopping: _LOGGER.warning("Disconnected from Rflink, reconnecting") - hass.async_create_task(connect()) + hass.async_create_task(connect(), eager_start=False) _reconnect_job = HassJob(reconnect, "Rflink reconnect", cancel_on_shutdown=True) @@ -312,7 +313,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.info("Connected to Rflink") - hass.async_create_task(connect()) + hass.async_create_task(connect(), eager_start=False) async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback) return True @@ -580,7 +581,7 @@ class RflinkCommand(RflinkDevice): if repetitions > 1: self._repetition_task = self.hass.async_create_task( - self._async_send_command(cmd, repetitions - 1) + self._async_send_command(cmd, repetitions - 1), eager_start=False ) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 4cacb27b49a..fb339f4ba5a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -25,7 +25,10 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import ( + DeviceInfo, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -259,7 +262,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: devices.pop(device_id) @callback - def _updated_device(event: Event) -> None: + def _updated_device(event: Event[EventDeviceRegistryUpdatedData]) -> None: if event.data["action"] != "remove": return device_entry = device_registry.deleted_devices[event.data["device_id"]] @@ -409,7 +412,7 @@ def find_possible_pt2262_device(device_ids: set[str], device_id: str) -> str | N for dev_id in device_ids: if len(dev_id) == len(device_id): size = None - for i, (char1, char2) in enumerate(zip(dev_id, device_id)): + for i, (char1, char2) in enumerate(zip(dev_id, device_id, strict=False)): if char1 != char2: break size = i diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 837ca554615..1fbb2e8fc29 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -30,14 +30,14 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, ) -from homeassistant.core import State, callback +from homeassistant.core import Event, EventStateChangedData, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from . import ( DOMAIN, @@ -353,10 +353,10 @@ class RfxtrxOptionsFlow(OptionsFlow): entity_migration_map[new_entity_id] = entry @callback - def _handle_state_removed( - entity_id: str, old_state: State | None, new_state: State | None - ) -> None: + def _handle_state_removed(event: Event[EventStateChangedData]) -> None: # Wait for entities to finish cleanup + new_state = event.data["new_state"] + entity_id = event.data["entity_id"] if new_state is None and entity_id in entities_to_be_removed: entities_to_be_removed.remove(entity_id) if not entities_to_be_removed: @@ -370,7 +370,7 @@ class RfxtrxOptionsFlow(OptionsFlow): if not self.hass.states.async_available(entry.entity_id) } wait_for_entities = asyncio.Event() - remove_track_state_changes = async_track_state_change( + remove_track_state_changes = async_track_state_change_event( self.hass, entities_to_be_removed, _handle_state_removed ) @@ -384,10 +384,10 @@ class RfxtrxOptionsFlow(OptionsFlow): remove_track_state_changes() @callback - def _handle_state_added( - entity_id: str, old_state: State | None, new_state: State | None - ) -> None: + def _handle_state_added(event: Event[EventStateChangedData]) -> None: # Wait for entities to be added + old_state = event.data["old_state"] + entity_id = event.data["entity_id"] if old_state is None and entity_id in entities_to_be_added: entities_to_be_added.remove(entity_id) if not entities_to_be_added: @@ -400,7 +400,7 @@ class RfxtrxOptionsFlow(OptionsFlow): if self.hass.states.async_available(entry.entity_id) } wait_for_entities = asyncio.Event() - remove_track_state_changes = async_track_state_change( + remove_track_state_changes = async_track_state_change_event( self.hass, entities_to_be_added, _handle_state_added ) @@ -486,7 +486,10 @@ class RfxtrxOptionsFlow(OptionsFlow): if devices: for event_code, options in devices.items(): if options is None: - entry_data[CONF_DEVICES].pop(event_code) + # If the config entry is setup, the device registry + # listener will remove the device from the config + # entry before we get here + entry_data[CONF_DEVICES].pop(event_code, None) else: entry_data[CONF_DEVICES][event_code] = options self.hass.config_entries.async_update_entry(self._config_entry, data=entry_data) diff --git a/homeassistant/components/rfxtrx/event.py b/homeassistant/components/rfxtrx/event.py index 7e73919aacd..5c3944dc74b 100644 --- a/homeassistant/components/rfxtrx/event.py +++ b/homeassistant/components/rfxtrx/event.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry +from .const import DEVICE_PACKET_TYPE_LIGHTING4 _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,10 @@ async def async_setup_entry( """Set up config entry.""" def _supported(event: RFXtrxEvent) -> bool: - return isinstance(event, (ControlEvent, SensorEvent)) + return ( + isinstance(event, (ControlEvent, SensorEvent)) + and event.device.packettype != DEVICE_PACKET_TYPE_LIGHTING4 + ) def _constructor( event: RFXtrxEvent, diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index ec902855f27..bb3701e2e31 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "iot_class": "local_push", "loggers": ["RFXtrx"], - "requirements": ["pyRFXtrx==0.31.0"] + "requirements": ["pyRFXtrx==0.31.1"] } diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index f421b6da7ef..46a3f021122 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -149,7 +149,7 @@ SENSOR_TYPES = ( translation_key="total_energy_usage", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index e3697d4fccc..36c66550ddc 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -2,34 +2,39 @@ from __future__ import annotations +from dataclasses import dataclass from functools import partial import logging +from typing import Any, cast -from ring_doorbell import Auth, Ring +from ring_doorbell import Auth, Ring, RingDevices 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.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import ( - DOMAIN, - PLATFORMS, - RING_API, - RING_DEVICES, - RING_DEVICES_COORDINATOR, - RING_NOTIFICATIONS_COORDINATOR, -) +from .const import DOMAIN, PLATFORMS from .coordinator import RingDataCoordinator, RingNotificationsCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class RingData: + """Class to support type hinting of ring data collection.""" + + api: Ring + devices: RingDevices + devices_coordinator: RingDataCoordinator + notifications_coordinator: RingNotificationsCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - def token_updater(token): + def token_updater(token: dict[str, Any]) -> None: """Handle from sync context when token is updated.""" hass.loop.call_soon_threadsafe( partial( @@ -44,17 +49,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ring = Ring(auth) + await _migrate_old_unique_ids(hass, entry.entry_id) + devices_coordinator = RingDataCoordinator(hass, ring) notifications_coordinator = RingNotificationsCoordinator(hass, ring) await devices_coordinator.async_config_entry_first_refresh() await notifications_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - RING_API: ring, - RING_DEVICES: ring.devices(), - RING_DEVICES_COORDINATOR: devices_coordinator, - RING_NOTIFICATIONS_COORDINATOR: notifications_coordinator, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RingData( + api=ring, + devices=ring.devices(), + devices_coordinator=devices_coordinator, + notifications_coordinator=notifications_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -81,8 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for info in hass.data[DOMAIN].values(): - await info[RING_DEVICES_COORDINATOR].async_refresh() - await info[RING_NOTIFICATIONS_COORDINATOR].async_refresh() + ring_data = cast(RingData, info) + await ring_data.devices_coordinator.async_refresh() + await ring_data.notifications_coordinator.async_refresh() # register service hass.services.async_register(DOMAIN, "update", async_refresh_all) @@ -111,3 +119,29 @@ async def async_remove_config_entry_device( ) -> bool: """Remove a config entry from a device.""" return True + + +async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None: + entity_registry = er.async_get(hass) + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, str] | None: + # Old format for camera and light was int + unique_id = cast(str | int, entity_entry.unique_id) + if isinstance(unique_id, int): + new_unique_id = str(unique_id) + if existing_entity_id := entity_registry.async_get_entity_id( + entity_entry.domain, entity_entry.platform, new_unique_id + ): + _LOGGER.error( + "Cannot migrate to unique_id '%s', already exists for '%s', " + "You may have to delete unavailable ring entities", + new_unique_id, + existing_entity_id, + ) + return None + _LOGGER.info("Fixing non string unique id %s", entity_entry.unique_id) + return {"new_unique_id": new_unique_id} + return None + + await er.async_migrate_entries(hass, entry_id, _async_migrator) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 19daebf9ce1..2db04cfd461 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,10 +2,13 @@ from __future__ import annotations +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import datetime from typing import Any +from ring_doorbell import Ring, RingEvent, RingGeneric + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -15,29 +18,32 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, RING_API, RING_DEVICES, RING_NOTIFICATIONS_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingNotificationsCoordinator -from .entity import RingEntity +from .entity import RingBaseEntity @dataclass(frozen=True, kw_only=True) class RingBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Ring binary sensor entity.""" - category: list[str] + exists_fn: Callable[[RingGeneric], bool] BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( RingBinarySensorEntityDescription( key="ding", translation_key="ding", - category=["doorbots", "authorized_doorbots", "other"], device_class=BinarySensorDeviceClass.OCCUPANCY, + exists_fn=lambda device: device.family + in {"doorbots", "authorized_doorbots", "other"}, ), RingBinarySensorEntityDescription( key="motion", - category=["doorbots", "authorized_doorbots", "stickup_cams"], device_class=BinarySensorDeviceClass.MOTION, + exists_fn=lambda device: device.family + in {"doorbots", "authorized_doorbots", "stickup_cams"}, ), ) @@ -48,34 +54,36 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ring binary sensors from a config entry.""" - ring = hass.data[DOMAIN][config_entry.entry_id][RING_API] - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - notifications_coordinator: RingNotificationsCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ][RING_NOTIFICATIONS_COORDINATOR] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] entities = [ - RingBinarySensor(ring, device, notifications_coordinator, description) - for device_type in ("doorbots", "authorized_doorbots", "stickup_cams", "other") + RingBinarySensor( + ring_data.api, + device, + ring_data.notifications_coordinator, + description, + ) for description in BINARY_SENSOR_TYPES - if device_type in description.category - for device in devices[device_type] + for device in ring_data.devices.all_devices + if description.exists_fn(device) ] async_add_entities(entities) -class RingBinarySensor(RingEntity, BinarySensorEntity): +class RingBinarySensor( + RingBaseEntity[RingNotificationsCoordinator], BinarySensorEntity +): """A binary sensor implementation for Ring device.""" - _active_alert: dict[str, Any] | None = None + _active_alert: RingEvent | None = None entity_description: RingBinarySensorEntityDescription def __init__( self, - ring, - device, - coordinator, + ring: Ring, + device: RingGeneric, + coordinator: RingNotificationsCoordinator, description: RingBinarySensorEntityDescription, ) -> None: """Initialize a sensor for Ring device.""" @@ -89,13 +97,13 @@ class RingBinarySensor(RingEntity, BinarySensorEntity): self._update_alert() @callback - def _handle_coordinator_update(self, _=None): + def _handle_coordinator_update(self, _: Any = None) -> None: """Call update method.""" self._update_alert() super()._handle_coordinator_update() @callback - def _update_alert(self): + def _update_alert(self) -> None: """Update active alert.""" self._active_alert = next( ( @@ -108,21 +116,23 @@ class RingBinarySensor(RingEntity, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return True if the binary sensor is on.""" return self._active_alert is not None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes.""" attrs = super().extra_state_attributes if self._active_alert is None: return attrs + assert isinstance(attrs, dict) attrs["state"] = self._active_alert["state"] - attrs["expires_at"] = datetime.fromtimestamp( - self._active_alert.get("now") + self._active_alert.get("expires_in") - ).isoformat() + now = self._active_alert.get("now") + expires_in = self._active_alert.get("expires_in") + assert now and expires_in + attrs["expires_at"] = datetime.fromtimestamp(now + expires_in).isoformat() return attrs diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py index 343c0d68257..15d56a8b7cf 100644 --- a/homeassistant/components/ring/button.py +++ b/homeassistant/components/ring/button.py @@ -2,12 +2,15 @@ from __future__ import annotations +from ring_doorbell import RingOther + 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 . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -22,25 +25,23 @@ async def async_setup_entry( 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 - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( RingDoorButton(device, devices_coordinator, BUTTON_DESCRIPTION) - for device in devices["other"] + for device in ring_data.devices.other if device.has_capability("open") ) -class RingDoorButton(RingEntity, ButtonEntity): +class RingDoorButton(RingEntity[RingOther], ButtonEntity): """Creates a button to open the ring intercom door.""" def __init__( self, - device, - coordinator, + device: RingOther, + coordinator: RingDataCoordinator, description: ButtonEntityDescription, ) -> None: """Initialize the button.""" diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 7cbe3559ab2..a5144777eaa 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -3,11 +3,12 @@ from __future__ import annotations from datetime import timedelta -from itertools import chain import logging -from typing import Optional +from typing import Any +from aiohttp import web from haffmpeg.camera import CameraMjpeg +from ring_doorbell import RingDoorBell from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera @@ -17,7 +18,8 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -33,50 +35,50 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Ring Door Bell and StickUp Camera.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass) - cams = [] - for camera in chain( - devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] - ): - if not camera.has_subscription: - continue - - cams.append(RingCam(camera, devices_coordinator, ffmpeg_manager)) + cams = [ + RingCam(camera, devices_coordinator, ffmpeg_manager) + for camera in ring_data.devices.video_devices + if camera.has_subscription + ] async_add_entities(cams) -class RingCam(RingEntity, Camera): +class RingCam(RingEntity[RingDoorBell], Camera): """An implementation of a Ring Door Bell camera.""" _attr_name = None - def __init__(self, device, coordinator, ffmpeg_manager): + def __init__( + self, + device: RingDoorBell, + coordinator: RingDataCoordinator, + ffmpeg_manager: ffmpeg.FFmpegManager, + ) -> None: """Initialize a Ring Door Bell camera.""" super().__init__(device, coordinator) Camera.__init__(self) - self._ffmpeg_manager = ffmpeg_manager - self._last_event = None - self._last_video_id = None - self._video_url = None - self._image = None + self._last_event: dict[str, Any] | None = None + self._last_video_id: int | None = None + self._video_url: str | None = None + self._image: bytes | None = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL - self._attr_unique_id = device.id + self._attr_unique_id = str(device.id) if device.has_capability(MOTION_DETECTION_CAPABILITY): self._attr_motion_detection_enabled = device.motion_detection @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" - history_data: Optional[list] - if not (history_data := self._get_coordinator_history()): - return + self._device = self._get_coordinator_data().get_video_device( + self._device.device_api_id + ) + history_data = self._device.last_history if history_data: self._last_event = history_data[0] self.async_schedule_update_ha_state(True) @@ -89,7 +91,7 @@ class RingCam(RingEntity, Camera): self.async_write_ha_state() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { "video_url": self._video_url, @@ -100,7 +102,7 @@ class RingCam(RingEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - if self._image is None and self._video_url: + if self._image is None and self._video_url is not None: image = await ffmpeg.async_get_image( self.hass, self._video_url, @@ -113,10 +115,12 @@ class RingCam(RingEntity, Camera): return self._image - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse | None: """Generate an HTTP MJPEG stream from the camera.""" if self._video_url is None: - return + return None stream = CameraMjpeg(self._ffmpeg_manager.binary) await stream.open_camera(self._video_url) @@ -160,11 +164,15 @@ class RingCam(RingEntity, Camera): self._expires_at = FORCE_REFRESH_INTERVAL + utcnow @exception_wrap - def _get_video(self) -> str: - return self._device.recording_url(self._last_event["id"]) + def _get_video(self) -> str | None: + if self._last_event is None: + return None + event_id = self._last_event.get("id") + assert event_id and isinstance(event_id, int) + return self._device.recording_url(event_id) @exception_wrap - def _set_motion_detection_enabled(self, new_state): + def _set_motion_detection_enabled(self, new_state: bool) -> None: if not self._device.has_capability(MOTION_DETECTION_CAPABILITY): _LOGGER.error( "Entity %s does not have motion detection capability", self.entity_id diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6d4f28eb311..4762017c5bc 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -28,7 +28,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) -async def validate_input(hass: HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]: """Validate the user input allows us to connect.""" auth = Auth(f"{APPLICATION_NAME}/{ha_version}") @@ -56,9 +56,11 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): user_pass: dict[str, Any] = {} reauth_entry: ConfigEntry | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: try: token = await validate_input(self.hass, user_input) @@ -82,7 +84,9 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_2fa(self, user_input=None): + async def async_step_2fa( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle 2fa step.""" if user_input: if self.reauth_entry: @@ -110,7 +114,7 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" - errors = {} + errors: dict[str, str] = {} assert self.reauth_entry is not None if user_input: diff --git a/homeassistant/components/ring/const.py b/homeassistant/components/ring/const.py index 23f378a38be..70813a78c76 100644 --- a/homeassistant/components/ring/const.py +++ b/homeassistant/components/ring/const.py @@ -28,10 +28,4 @@ PLATFORMS = [ SCAN_INTERVAL = timedelta(minutes=1) NOTIFICATIONS_SCAN_INTERVAL = timedelta(seconds=5) -RING_API = "api" -RING_DEVICES = "devices" - -RING_DEVICES_COORDINATOR = "device_data" -RING_NOTIFICATIONS_COORDINATOR = "dings_data" - CONF_2FA = "2fa" diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index fdb6fc1f296..a10f9317bab 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -2,11 +2,10 @@ from asyncio import TaskGroup from collections.abc import Callable -from dataclasses import dataclass import logging -from typing import Any, Optional +from typing import TypeVar, TypeVarTuple -from ring_doorbell import AuthenticationError, Ring, RingError, RingGeneric, RingTimeout +from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -16,10 +15,13 @@ from .const import NOTIFICATIONS_SCAN_INTERVAL, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) +_R = TypeVar("_R") +_Ts = TypeVarTuple("_Ts") + async def _call_api( - hass: HomeAssistant, target: Callable[..., Any], *args, msg_suffix: str = "" -): + hass: HomeAssistant, target: Callable[[*_Ts], _R], *args: *_Ts, msg_suffix: str = "" +) -> _R: try: return await hass.async_add_executor_job(target, *args) except AuthenticationError as err: @@ -34,15 +36,7 @@ async def _call_api( raise UpdateFailed(f"Error communicating with API{msg_suffix}: {err}") from err -@dataclass -class RingDeviceData: - """RingDeviceData.""" - - device: RingGeneric - history: Optional[list] = None - - -class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): +class RingDataCoordinator(DataUpdateCoordinator[RingDevices]): """Base class for device coordinators.""" def __init__( @@ -60,45 +54,39 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): self.ring_api: Ring = ring_api self.first_call: bool = True - async def _async_update_data(self): + async def _async_update_data(self) -> RingDevices: """Fetch data from API endpoint.""" update_method: str = "update_data" if self.first_call else "update_devices" await _call_api(self.hass, getattr(self.ring_api, update_method)) self.first_call = False - data: dict[str, RingDeviceData] = {} - devices: dict[str : list[RingGeneric]] = self.ring_api.devices() + devices: RingDevices = self.ring_api.devices() subscribed_device_ids = set(self.async_contexts()) - for device_type in devices: - for device in devices[device_type]: - # Don't update all devices in the ring api, only those that set - # their device id as context when they subscribed. - if device.id in subscribed_device_ids: - data[device.id] = RingDeviceData(device=device) - try: - history_task = None - async with TaskGroup() as tg: - if device.has_capability("history"): - history_task = tg.create_task( - _call_api( - self.hass, - lambda device: device.history(limit=10), - device, - msg_suffix=f" for device {device.name}", # device_id is the mac - ) - ) + for device in devices.all_devices: + # Don't update all devices in the ring api, only those that set + # their device id as context when they subscribed. + if device.id in subscribed_device_ids: + try: + async with TaskGroup() as tg: + if device.has_capability("history"): tg.create_task( _call_api( self.hass, - device.update_health_data, - msg_suffix=f" for device {device.name}", + lambda device: device.history(limit=10), + device, + msg_suffix=f" for device {device.name}", # device_id is the mac ) ) - if history_task: - data[device.id].history = history_task.result() - except ExceptionGroup as eg: - raise eg.exceptions[0] # noqa: B904 + tg.create_task( + _call_api( + self.hass, + device.update_health_data, + msg_suffix=f" for device {device.name}", + ) + ) + except ExceptionGroup as eg: + raise eg.exceptions[0] # noqa: B904 - return data + return devices class RingNotificationsCoordinator(DataUpdateCoordinator[None]): @@ -114,6 +102,6 @@ class RingNotificationsCoordinator(DataUpdateCoordinator[None]): ) self.ring_api: Ring = ring_api - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" await _call_api(self.hass, self.ring_api.update_dings) diff --git a/homeassistant/components/ring/diagnostics.py b/homeassistant/components/ring/diagnostics.py index 5295629979a..2e7604d9f50 100644 --- a/homeassistant/components/ring/diagnostics.py +++ b/homeassistant/components/ring/diagnostics.py @@ -4,12 +4,11 @@ from __future__ import annotations from typing import Any -from ring_doorbell import Ring - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from . import RingData from .const import DOMAIN TO_REDACT = { @@ -33,11 +32,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - ring: Ring = hass.data[DOMAIN][entry.entry_id]["api"] + ring_data: RingData = hass.data[DOMAIN][entry.entry_id] + devices_data = ring_data.api.devices_data 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] + devices_data[device_type][device_id] + for device_type in devices_data + for device_id in devices_data[device_type] ] return async_redact_data( {"device_data": devices_raw}, diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index fb617ecd7d1..65ccbb8ece4 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -1,9 +1,16 @@ """Base class for Ring entity.""" from collections.abc import Callable -from typing import Any, Concatenate, ParamSpec, TypeVar +from typing import Any, Concatenate, Generic, ParamSpec, cast -from ring_doorbell import AuthenticationError, RingError, RingGeneric, RingTimeout +from ring_doorbell import ( + AuthenticationError, + RingDevices, + RingError, + RingGeneric, + RingTimeout, +) +from typing_extensions import TypeVar from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -11,26 +18,25 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN -from .coordinator import ( - RingDataCoordinator, - RingDeviceData, - RingNotificationsCoordinator, -) +from .coordinator import RingDataCoordinator, RingNotificationsCoordinator + +RingDeviceT = TypeVar("RingDeviceT", bound=RingGeneric, default=RingGeneric) _RingCoordinatorT = TypeVar( "_RingCoordinatorT", bound=(RingDataCoordinator | RingNotificationsCoordinator), ) -_T = TypeVar("_T", bound="RingEntity") +_RingBaseEntityT = TypeVar("_RingBaseEntityT", bound="RingBaseEntity[Any, Any]") +_R = TypeVar("_R") _P = ParamSpec("_P") def exception_wrap( - func: Callable[Concatenate[_T, _P], Any], -) -> Callable[Concatenate[_T, _P], Any]: + func: Callable[Concatenate[_RingBaseEntityT, _P], _R], +) -> Callable[Concatenate[_RingBaseEntityT, _P], _R]: """Define a wrapper to catch exceptions and raise HomeAssistant errors.""" - def _wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R: try: return func(self, *args, **kwargs) except AuthenticationError as err: @@ -50,7 +56,9 @@ def exception_wrap( return _wrap -class RingEntity(CoordinatorEntity[_RingCoordinatorT]): +class RingBaseEntity( + CoordinatorEntity[_RingCoordinatorT], Generic[_RingCoordinatorT, RingDeviceT] +): """Base implementation for Ring device.""" _attr_attribution = ATTRIBUTION @@ -59,7 +67,7 @@ class RingEntity(CoordinatorEntity[_RingCoordinatorT]): def __init__( self, - device: RingGeneric, + device: RingDeviceT, coordinator: _RingCoordinatorT, ) -> None: """Initialize a sensor for Ring device.""" @@ -73,29 +81,17 @@ class RingEntity(CoordinatorEntity[_RingCoordinatorT]): name=device.name, ) - def _get_coordinator_device_data(self) -> RingDeviceData | None: - if (data := self.coordinator.data) and ( - device_data := data.get(self._device.id) - ): - return device_data - return None - def _get_coordinator_device(self) -> RingGeneric | None: - if (device_data := self._get_coordinator_device_data()) and ( - device := device_data.device - ): - return device - return None +class RingEntity(RingBaseEntity[RingDataCoordinator, RingDeviceT]): + """Implementation for Ring devices.""" - def _get_coordinator_history(self) -> list | None: - if (device_data := self._get_coordinator_device_data()) and ( - history := device_data.history - ): - return history - return None + def _get_coordinator_data(self) -> RingDevices: + return self.coordinator.data @callback def _handle_coordinator_update(self) -> None: - if device := self._get_coordinator_device(): - self._device = device + self._device = cast( + RingDeviceT, + self._get_coordinator_data().get_device(self._device.device_api_id), + ) super()._handle_coordinator_update() diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 31e22c2084c..5747c9e77f7 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,10 +1,11 @@ """Component providing HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta +from enum import StrEnum, auto import logging from typing import Any -from ring_doorbell import RingGeneric, RingStickUpCam +from ring_doorbell import RingStickUpCam from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -12,7 +13,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -26,8 +28,12 @@ _LOGGER = logging.getLogger(__name__) SKIP_UPDATES_DELAY = timedelta(seconds=5) -ON_STATE = "on" -OFF_STATE = "off" + +class OnOffState(StrEnum): + """Enum for allowed on off states.""" + + ON = auto() + OFF = auto() async def async_setup_entry( @@ -36,56 +42,56 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the lights 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 - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( RingLight(device, devices_coordinator) - for device in devices["stickup_cams"] + for device in ring_data.devices.stickup_cams if device.has_capability("light") ) -class RingLight(RingEntity, LightEntity): +class RingLight(RingEntity[RingStickUpCam], LightEntity): """Creates a switch to turn the ring cameras light on and off.""" _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} _attr_translation_key = "light" - def __init__(self, device: RingGeneric, coordinator) -> None: + def __init__( + self, device: RingStickUpCam, coordinator: RingDataCoordinator + ) -> None: """Initialize the light.""" super().__init__(device, coordinator) - self._attr_unique_id = device.id - self._attr_is_on = device.lights == ON_STATE + self._attr_unique_id = str(device.id) + self._attr_is_on = device.lights == OnOffState.ON self._no_updates_until = dt_util.utcnow() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" if self._no_updates_until > dt_util.utcnow(): return - if (device := self._get_coordinator_device()) and isinstance( - device, RingStickUpCam - ): - self._attr_is_on = device.lights == ON_STATE + device = self._get_coordinator_data().get_stickup_cam( + self._device.device_api_id + ) + self._attr_is_on = device.lights == OnOffState.ON super()._handle_coordinator_update() @exception_wrap - def _set_light(self, new_state): + def _set_light(self, new_state: OnOffState) -> None: """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state - self._attr_is_on = new_state == ON_STATE + self._attr_is_on = new_state == OnOffState.ON self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() def turn_on(self, **kwargs: Any) -> None: """Turn the light on for 30 seconds.""" - self._set_light(ON_STATE) + self._set_light(OnOffState.ON) def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - self._set_light(OFF_STATE) + self._set_light(OnOffState.OFF) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 67e2cfcdc78..63417f90b42 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,6 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.9"] + "quality_scale": "silver", + "requirements": ["ring-doorbell[listen]==0.8.11"] } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 9ba677e7e5b..b6849e37d96 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -2,10 +2,18 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, Generic, cast -from ring_doorbell import RingGeneric +from ring_doorbell import ( + RingCapability, + RingChime, + RingDoorBell, + RingEventKind, + RingGeneric, + RingOther, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,10 +29,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator -from .entity import RingEntity +from .entity import RingDeviceT, RingEntity async def async_setup_entry( @@ -33,209 +43,192 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a sensor for a Ring device.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator entities = [ - description.cls(device, devices_coordinator, description) - for device_type in ( - "chimes", - "doorbots", - "authorized_doorbots", - "stickup_cams", - "other", - ) + RingSensor(device, devices_coordinator, description) for description in SENSOR_TYPES - if device_type in description.category - for device in devices[device_type] - if not (device_type == "battery" and device.battery_life is None) + for device in ring_data.devices.all_devices + if description.exists_fn(device) ] async_add_entities(entities) -class RingSensor(RingEntity, SensorEntity): +class RingSensor(RingEntity[RingDeviceT], SensorEntity): """A sensor implementation for Ring device.""" - entity_description: RingSensorEntityDescription + entity_description: RingSensorEntityDescription[RingDeviceT] def __init__( self, - device: RingGeneric, + device: RingDeviceT, coordinator: RingDataCoordinator, - description: RingSensorEntityDescription, + description: RingSensorEntityDescription[RingDeviceT], ) -> None: """Initialize a sensor for Ring device.""" super().__init__(device, coordinator) self.entity_description = description self._attr_unique_id = f"{device.id}-{description.key}" - - @property - def native_value(self): - """Return the state of the sensor.""" - 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 - - -class HealthDataRingSensor(RingSensor): - """Ring sensor that relies on health data.""" - - # These sensors are data hungry and not useful. Disable by default. - _attr_entity_registry_enabled_default = False - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "wifi_signal_category": - return self._device.wifi_signal_category - - if sensor_type == "wifi_signal_strength": - return self._device.wifi_signal_strength - - -class HistoryRingSensor(RingSensor): - """Ring sensor that relies on history data.""" - - _latest_event: dict[str, Any] | None = None + self._attr_entity_registry_enabled_default = ( + description.entity_registry_enabled_default + ) + self._attr_native_value = self.entity_description.value_fn(self._device) @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" - if not (history_data := self._get_coordinator_history()): - return - kind = self.entity_description.kind - found = None - if kind is None: - found = history_data[0] - else: - for entry in history_data: - if entry["kind"] == kind: - found = entry - break - - if not found: - return - - self._latest_event = found + self._device = cast( + RingDeviceT, + self._get_coordinator_data().get_device(self._device.device_api_id), + ) + # History values can drop off the last 10 events so only update + # the value if it's not None + if native_value := self.entity_description.value_fn(self._device): + self._attr_native_value = native_value + if extra_attrs := self.entity_description.extra_state_attributes_fn( + self._device + ): + self._attr_extra_state_attributes = extra_attrs super()._handle_coordinator_update() - @property - def native_value(self): - """Return the state of the sensor.""" - if self._latest_event is None: - return None - return self._latest_event["created_at"] +def _get_last_event( + history_data: list[dict[str, Any]], kind: RingEventKind | None +) -> dict[str, Any] | None: + if not history_data: + return None + if kind is None: + return history_data[0] + for entry in history_data: + if entry["kind"] == kind.value: + return entry + return None - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attrs = super().extra_state_attributes - if self._latest_event: - attrs["created_at"] = self._latest_event["created_at"] - attrs["answered"] = self._latest_event["answered"] - attrs["recording_status"] = self._latest_event["recording"]["status"] - attrs["category"] = self._latest_event["kind"] - - return attrs +def _get_last_event_attrs( + history_data: list[dict[str, Any]], kind: RingEventKind | None +) -> dict[str, Any] | None: + if last_event := _get_last_event(history_data, kind): + return { + "created_at": last_event.get("created_at"), + "answered": last_event.get("answered"), + "recording_status": last_event.get("recording", {}).get("status"), + "category": last_event.get("kind"), + } + return None @dataclass(frozen=True, kw_only=True) -class RingSensorEntityDescription(SensorEntityDescription): +class RingSensorEntityDescription(SensorEntityDescription, Generic[RingDeviceT]): """Describes Ring sensor entity.""" - category: list[str] - cls: type[RingSensor] - - kind: str | None = None + value_fn: Callable[[RingDeviceT], StateType] = lambda _: True + exists_fn: Callable[[RingGeneric], bool] = lambda _: True + extra_state_attributes_fn: Callable[[RingDeviceT], dict[str, Any] | None] = ( + lambda _: None + ) -SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( - RingSensorEntityDescription( +# For some reason mypy doesn't properly type check the default TypeVar value here +# so for now the [RingGeneric] subscript needs to be specified. +# Once https://github.com/python/mypy/issues/14851 is closed this should hopefully +# be fixed and the [RingGeneric] subscript can be removed. +# https://github.com/home-assistant/core/pull/115276#discussion_r1560106576 +SENSOR_TYPES: tuple[RingSensorEntityDescription[Any], ...] = ( + RingSensorEntityDescription[RingGeneric]( key="battery", - category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - cls=RingSensor, + value_fn=lambda device: device.battery_life, + exists_fn=lambda device: device.family != "chimes", ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_activity", translation_key="last_activity", - category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, None)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if (last_event_attrs := _get_last_event_attrs(device.last_history, None)) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_ding", translation_key="last_ding", - category=["doorbots", "authorized_doorbots", "other"], - kind="ding", device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, RingEventKind.DING)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if ( + last_event_attrs := _get_last_event_attrs( + device.last_history, RingEventKind.DING + ) + ) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_motion", translation_key="last_motion", - category=["doorbots", "authorized_doorbots", "stickup_cams"], - kind="motion", device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, RingEventKind.MOTION)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if ( + last_event_attrs := _get_last_event_attrs( + device.last_history, RingEventKind.MOTION + ) + ) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingDoorBell | RingChime]( key="volume", translation_key="volume", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - cls=RingSensor, + value_fn=lambda device: device.volume, + exists_fn=lambda device: isinstance(device, (RingDoorBell, RingChime)), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="doorbell_volume", translation_key="doorbell_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.doorbell_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="mic_volume", translation_key="mic_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.mic_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="voice_volume", translation_key="voice_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.voice_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="wifi_signal_category", translation_key="wifi_signal_category", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], entity_category=EntityCategory.DIAGNOSTIC, - cls=HealthDataRingSensor, + entity_registry_enabled_default=False, + value_fn=lambda device: device.wifi_signal_category, ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="wifi_signal_strength", translation_key="wifi_signal_strength", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, - cls=HealthDataRingSensor, + entity_registry_enabled_default=False, + value_fn=lambda device: device.wifi_signal_strength, ), ) diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 4e53ab8a006..f63f9d33182 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -3,15 +3,15 @@ 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 import RingChime, RingEventKind from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature 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 . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -24,24 +24,23 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the sirens for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( - RingChimeSiren(device, coordinator) for device in devices["chimes"] + RingChimeSiren(device, devices_coordinator) + for device in ring_data.devices.chimes ) -class RingChimeSiren(RingEntity, SirenEntity): +class RingChimeSiren(RingEntity[RingChime], SirenEntity): """Creates a siren to play the test chimes of a Chime device.""" - _attr_available_tones = CHIME_TEST_SOUND_KINDS + _attr_available_tones = [RingEventKind.DING.value, RingEventKind.MOTION.value] _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _attr_translation_key = "siren" - def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None: + def __init__(self, device: RingChime, coordinator: RingDataCoordinator) -> None: """Initialize a Ring Chime siren.""" super().__init__(device, coordinator) # Entity class attributes @@ -50,6 +49,6 @@ class RingChimeSiren(RingEntity, SirenEntity): @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 + tone = kwargs.get(ATTR_TONE) or RingEventKind.DING.value self._device.test_sound(kind=tone) diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 15aa0a787bb..0e032907bae 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import Any -from ring_doorbell import RingGeneric, RingStickUpCam +from ring_doorbell import RingStickUpCam from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -12,7 +12,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -33,23 +34,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the switches for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( - SirenSwitch(device, coordinator) - for device in devices["stickup_cams"] + SirenSwitch(device, devices_coordinator) + for device in ring_data.devices.stickup_cams if device.has_capability("siren") ) -class BaseRingSwitch(RingEntity, SwitchEntity): +class BaseRingSwitch(RingEntity[RingStickUpCam], SwitchEntity): """Represents a switch for controlling an aspect of a ring device.""" def __init__( - self, device: RingGeneric, coordinator: RingDataCoordinator, device_type: str + self, device: RingStickUpCam, coordinator: RingDataCoordinator, device_type: str ) -> None: """Initialize the switch.""" super().__init__(device, coordinator) @@ -62,26 +61,27 @@ class SirenSwitch(BaseRingSwitch): _attr_translation_key = "siren" - def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None: + def __init__( + self, device: RingStickUpCam, coordinator: RingDataCoordinator + ) -> None: """Initialize the switch for a device with a siren.""" super().__init__(device, coordinator, "siren") self._no_updates_until = dt_util.utcnow() self._attr_is_on = device.siren > 0 @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" if self._no_updates_until > dt_util.utcnow(): return - - if (device := self._get_coordinator_device()) and isinstance( - device, RingStickUpCam - ): - self._attr_is_on = device.siren > 0 + device = self._get_coordinator_data().get_stickup_cam( + self._device.device_api_id + ) + self._attr_is_on = device.siren > 0 super()._handle_coordinator_update() @exception_wrap - def _set_switch(self, new_state): + def _set_switch(self, new_state: int) -> None: """Update switch state, and causes Home Assistant to correctly update.""" self._device.siren = new_state diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 5822177a243..21761e23d09 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -105,9 +105,9 @@ async def validate_local_input( ) try: await risco.connect() - except CannotConnectError as e: + except CannotConnectError: if comm_delay >= MAX_COMMUNICATION_DELAY: - raise e + raise comm_delay += 1 else: break diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 4c590b95e52..22e73a10d6d 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.6.0"] + "requirements": ["pyrisco==0.6.1"] } diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index f4d6ddaf451..8f97c76c879 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -56,8 +56,8 @@ async def async_setup_entry( config_entry.entry_id ][EVENTS_COORDINATOR] sensors = [ - RiscoSensor(coordinator, id, [], name, config_entry.entry_id) - for id, name in CATEGORIES.items() + RiscoSensor(coordinator, category_id, [], name, config_entry.entry_id) + for category_id, name in CATEGORIES.items() ] sensors.append( RiscoSensor( diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index c01d1fc7c9b..12a884dba48 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -8,9 +8,9 @@ from datetime import timedelta import logging from typing import Any -from roborock import RoborockException, RoborockInvalidCredentials -from roborock.cloud_api import RoborockMqttClient +from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData +from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from roborock.web_api import RoborockApiClient from homeassistant.config_entries import ConfigEntry @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up roborock from a config entry.""" + _LOGGER.debug("Integration async setup entry: %s", entry.as_dict()) user_data = UserData.from_dict(entry.data[CONF_USER_DATA]) @@ -56,7 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } # Get a Coordinator if the device is available or if we have connected to the device before coordinators = await asyncio.gather( - *build_setup_functions(hass, device_map, user_data, product_info), + *build_setup_functions( + hass, device_map, user_data, product_info, home_data.rooms + ), return_exceptions=True, ) # Valid coordinators are those where we had networking cached or we could get networking @@ -72,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="no_coordinators", ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - coordinator.roborock_device_info.device.duid: coordinator + coordinator.api.device_info.device.duid: coordinator for coordinator in valid_coordinators } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -85,10 +88,13 @@ def build_setup_functions( device_map: dict[str, HomeDataDevice], user_data: UserData, product_info: dict[str, HomeDataProduct], + home_data_rooms: list[HomeDataRoom], ) -> list[Coroutine[Any, Any, RoborockDataUpdateCoordinator | None]]: """Create a list of setup functions that can later be called asynchronously.""" return [ - setup_device(hass, user_data, device, product_info[device.product_id]) + setup_device( + hass, user_data, device, product_info[device.product_id], home_data_rooms + ) for device in device_map.values() ] @@ -98,9 +104,12 @@ async def setup_device( user_data: UserData, device: HomeDataDevice, product_info: HomeDataProduct, + home_data_rooms: list[HomeDataRoom], ) -> RoborockDataUpdateCoordinator | None: """Set up a device Coordinator.""" - mqtt_client = RoborockMqttClient(user_data, DeviceData(device, product_info.name)) + mqtt_client = RoborockMqttClientV1( + user_data, DeviceData(device, product_info.model) + ) try: networking = await mqtt_client.get_networking() if networking is None: @@ -115,9 +124,9 @@ async def setup_device( ) _LOGGER.debug(err) await mqtt_client.async_release() - raise err + raise coordinator = RoborockDataUpdateCoordinator( - hass, device, networking, product_info, mqtt_client + hass, device, networking, product_info, mqtt_client, home_data_rooms ) # Verify we can communicate locally - if we can't, switch to cloud api await coordinator.verify_api() @@ -131,7 +140,7 @@ async def setup_device( await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady as ex: await coordinator.release() - if isinstance(coordinator.api, RoborockMqttClient): + if isinstance(coordinator.api, RoborockMqttClientV1): _LOGGER.warning( "Not setting up %s because the we failed to get data for the first time using the online client. " "Please ensure your Home Assistant instance can communicate with this device. " diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index ede9afc826d..5715aba3bba 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -69,11 +69,11 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "unknown_url" except RoborockInvalidEmail: errors["base"] = "invalid_email_format" - except RoborockException as ex: - _LOGGER.exception(ex) + except RoborockException: + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown_roborock" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception(ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return errors @@ -92,11 +92,11 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): login_data = await self._client.code_login(code) except RoborockInvalidCode: errors["base"] = "invalid_code" - except RoborockException as ex: - _LOGGER.exception(ex) + except RoborockException: + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown_roborock" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception(ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: if self.reauth_entry is not None: diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 77f0be3363e..6b1ed975fca 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -30,3 +30,5 @@ IMAGE_DRAWABLES: list[Drawable] = [ IMAGE_CACHE_INTERVAL = 90 MAP_SLEEP = 3 + +GET_MAPS_SERVICE_NAME = "get_maps" diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index e682b119069..32b7a487ac8 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -2,14 +2,16 @@ from __future__ import annotations +import asyncio from datetime import timedelta import logging -from roborock.cloud_api import RoborockMqttClient +from roborock import HomeDataRoom from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException -from roborock.local_api import RoborockLocalClient from roborock.roborock_typing import DeviceProp +from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1 +from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import HomeAssistant @@ -18,7 +20,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .models import RoborockHassDeviceInfo +from .models import RoborockHassDeviceInfo, RoborockMapInfo SCAN_INTERVAL = timedelta(seconds=30) @@ -34,7 +36,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): device: HomeDataDevice, device_networking: NetworkInfo, product_info: HomeDataProduct, - cloud_api: RoborockMqttClient, + cloud_api: RoborockMqttClientV1, + home_data_rooms: list[HomeDataRoom], ) -> None: """Initialize.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) @@ -45,8 +48,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): DeviceProp(), ) device_data = DeviceData(device, product_info.model, device_networking.ip) - self.api: RoborockLocalClient | RoborockMqttClient = RoborockLocalClient( - device_data + self.api: RoborockLocalClientV1 | RoborockMqttClientV1 = RoborockLocalClientV1( + device_data, queue_timeout=5 ) self.cloud_api = cloud_api self.device_info = DeviceInfo( @@ -61,11 +64,12 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): if mac := self.roborock_device_info.network_info.mac: self.device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, mac)} # Maps from map flag to map name - self.maps: dict[int, str] = {} + self.maps: dict[int, RoborockMapInfo] = {} + self._home_data_rooms = {str(room.id): room.name for room in home_data_rooms} async def verify_api(self) -> None: """Verify that the api is reachable. If it is not, switch clients.""" - if isinstance(self.api, RoborockLocalClient): + if isinstance(self.api, RoborockLocalClientV1): try: await self.api.ping() except RoborockException: @@ -95,7 +99,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def _async_update_data(self) -> DeviceProp: """Update data via library.""" try: - await self._update_device_prop() + await asyncio.gather(*(self._update_device_prop(), self.get_rooms())) self._set_current_map() except RoborockException as ex: raise UpdateFailed(ex) from ex @@ -117,4 +121,19 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): maps = await self.api.get_multi_maps_list() if maps and maps.map_info: for roborock_map in maps.map_info: - self.maps[roborock_map.mapFlag] = roborock_map.name + self.maps[roborock_map.mapFlag] = RoborockMapInfo( + flag=roborock_map.mapFlag, name=roborock_map.name, rooms={} + ) + + async def get_rooms(self) -> None: + """Get all of the rooms for the current map.""" + # The api is only able to access rooms for the currently selected map + # So it is important this is only called when you have the map you care + # about selected. + if self.current_map in self.maps: + iot_rooms = await self.api.get_room_mapping() + if iot_rooms is not None: + for room in iot_rooms: + self.maps[self.current_map].rooms[room.segment_id] = ( + self._home_data_rooms.get(room.iot_id, "Unknown") + ) diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 7affaa396e6..6450d849859 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,21 +2,21 @@ from typing import Any -from roborock.api import AttributeCache, RoborockClient -from roborock.cloud_api import RoborockMqttClient from roborock.command_cache import CacheableAttribute from roborock.containers import Consumable, Status from roborock.exceptions import RoborockException from roborock.roborock_message import RoborockDataProtocol from roborock.roborock_typing import RoborockCommand +from roborock.version_1_apis.roborock_client_v1 import AttributeCache, RoborockClientV1 +from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import RoborockDataUpdateCoordinator from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator class RoborockEntity(Entity): @@ -28,7 +28,7 @@ class RoborockEntity(Entity): self, unique_id: str, device_info: DeviceInfo, - api: RoborockClient, + api: RoborockClientV1, ) -> None: """Initialize the coordinated Roborock Device.""" self._attr_unique_id = unique_id @@ -36,7 +36,7 @@ class RoborockEntity(Entity): self._api = api @property - def api(self) -> RoborockClient: + def api(self) -> RoborockClientV1: """Returns the api.""" return self._api @@ -116,7 +116,7 @@ class RoborockCoordinatedEntity( return data.status @property - def cloud_api(self) -> RoborockMqttClient: + def cloud_api(self) -> RoborockMqttClientV1: """Return the cloud api.""" return self.coordinator.cloud_api @@ -137,4 +137,4 @@ class RoborockCoordinatedEntity( else: self.coordinator.roborock_device_info.props.consumable = value self.coordinator.data = self.coordinator.roborock_device_info.props - self.async_write_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 43e7f185433..babde739775 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -105,5 +105,8 @@ "default": "mdi:power-plug-off" } } + }, + "services": { + "get_maps": "mdi:floor-plan" } } diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 3367f1b3017..2aef39ce59b 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -66,17 +66,26 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): ) self._attr_image_last_updated = dt_util.utcnow() self.map_flag = map_flag - self.cached_map = self._create_image(starting_map) + try: + self.cached_map = self._create_image(starting_map) + except HomeAssistantError: + # If we failed to update the image on init, we set cached_map to empty bytes so that we are unavailable and can try again later. + self.cached_map = b"" self._attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def available(self): + """Determines if the entity is available.""" + return self.cached_map != b"" + @property def is_selected(self) -> bool: """Return if this map is the currently selected map.""" return self.map_flag == self.coordinator.current_map def is_map_valid(self) -> bool: - """Update this map if it is the current active map, and the vacuum is cleaning.""" - return ( + """Update this map if it is the current active map, and the vacuum is cleaning or if it has never been set at all.""" + return self.cached_map == b"" or ( self.is_selected and self.image_last_updated is not None and self.coordinator.roborock_device_info.props.status is not None @@ -96,7 +105,16 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): async def async_image(self) -> bytes | None: """Update the image if it is not cached.""" if self.is_map_valid(): - map_data: bytes = await self.cloud_api.get_map_v1() + response = await asyncio.gather( + *(self.cloud_api.get_map_v1(), self.coordinator.get_rooms()), + return_exceptions=True, + ) + if not isinstance(response[0], bytes): + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="map_failure", + ) + map_data = response[0] self.cached_map = self._create_image(map_data) return self.cached_map @@ -130,23 +148,28 @@ async def create_coordinator_maps( maps_info = sorted( coord.maps.items(), key=lambda data: data[0] == cur_map, reverse=True ) - for map_flag, map_name in maps_info: + for map_flag, map_info in maps_info: # Load the map - so we can access it with get_map_v1 if map_flag != cur_map: # Only change the map and sleep if we have multiple maps. await coord.api.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag]) + coord.current_map = map_flag # We cannot get the map until the roborock servers fully process the # map change. await asyncio.sleep(MAP_SLEEP) # Get the map data - api_data: bytes = await coord.cloud_api.get_map_v1() + map_update = await asyncio.gather( + *[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True + ) + # If we fail to get the map -> We should set it to empty byte, still create it, and set it as unavailable. + api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b"" entities.append( RoborockMap( - f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_name}", + f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_info.name}", coord, map_flag, api_data, - map_name, + map_info.name, ) ) if len(coord.maps) != 1: @@ -154,4 +177,5 @@ async def create_coordinator_maps( # does not change the end user's app. # Only needs to happen when we changed maps above. await coord.cloud_api.send_command(RoborockCommand.LOAD_MULTI_MAP, [cur_map]) + coord.current_map = cur_map return entities diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index a7a7fe01d23..0646f8ee083 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==0.40.0", - "vacuum-map-parser-roborock==0.1.1" + "python-roborock==2.0.0", + "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index 45b98fddbc5..b516c0ee05c 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -24,3 +24,12 @@ class RoborockHassDeviceInfo: "product": self.product.as_dict(), "props": self.props.as_dict(), } + + +@dataclass +class RoborockMapInfo: + """A model to describe all information about a map we may want.""" + + flag: int + name: str + rooms: dict[int, str] diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 09030ef8500..f761d0b2274 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -6,9 +6,9 @@ from dataclasses import dataclass import logging from typing import Any -from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute from roborock.exceptions import RoborockException +from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry @@ -73,7 +73,9 @@ async def async_setup_entry( return_exceptions=True, ) valid_entities: list[RoborockNumberEntity] = [] - for (coordinator, description), result in zip(possible_entities, results): + for (coordinator, description), result in zip( + possible_entities, results, strict=False + ): if result is None or isinstance(result, RoborockException): _LOGGER.debug("Not adding entity because of %s", result) else: diff --git a/homeassistant/components/roborock/services.yaml b/homeassistant/components/roborock/services.yaml new file mode 100644 index 00000000000..18de5c98c7b --- /dev/null +++ b/homeassistant/components/roborock/services.yaml @@ -0,0 +1,4 @@ +get_maps: + target: + entity: + domain: vacuum diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 7c457a1935b..30aa64f626a 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -293,5 +293,11 @@ "no_coordinators": { "message": "No devices were able to successfully setup" } + }, + "services": { + "get_maps": { + "name": "Get maps", + "description": "Get the map and room information of your device." + } } } diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 9c7ca3cdcae..694bf864809 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -8,8 +8,8 @@ from dataclasses import dataclass import logging from typing import Any -from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute +from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -121,7 +121,9 @@ async def async_setup_entry( return_exceptions=True, ) valid_entities: list[RoborockSwitch] = [] - for (coordinator, description), result in zip(possible_entities, results): + for (coordinator, description), result in zip( + possible_entities, results, strict=False + ): if result is None or isinstance(result, Exception): _LOGGER.debug("Not adding entity because of %s", result) else: diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 9a3cac86425..7c9c08bce4d 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -8,9 +8,9 @@ from datetime import time import logging from typing import Any -from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute from roborock.exceptions import RoborockException +from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.time import TimeEntity, TimeEntityDescription from homeassistant.config_entries import ConfigEntry @@ -137,7 +137,9 @@ async def async_setup_entry( return_exceptions=True, ) valid_entities: list[RoborockTimeEntity] = [] - for (coordinator, description), result in zip(possible_entities, results): + for (coordinator, description), result in zip( + possible_entities, results, strict=False + ): if result is None or isinstance(result, RoborockException): _LOGGER.debug("Not adding entity because of %s", result) else: diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 22d9353e2a2..16cf518aa02 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -1,5 +1,6 @@ """Support for Roborock vacuum class.""" +from dataclasses import asdict from typing import Any from roborock.code_mappings import RoborockStateCode @@ -17,11 +18,12 @@ from homeassistant.components.vacuum import ( VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from .const import DOMAIN +from .const import DOMAIN, GET_MAPS_SERVICE_NAME from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity @@ -66,6 +68,15 @@ async def async_setup_entry( for device_id, coordinator in coordinators.items() ) + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + GET_MAPS_SERVICE_NAME, + {}, + RoborockVacuum.get_maps.__name__, + supports_response=SupportsResponse.ONLY, + ) + class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): """General Representation of a Roborock vacuum.""" @@ -164,3 +175,11 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): ) -> None: """Send a command to a vacuum cleaner.""" await self.send(command, params) + + async def get_maps(self) -> ServiceResponse: + """Get map information such as map id and room ids.""" + return { + "maps": [ + asdict(vacuum_map) for vacuum_map in self.coordinator.maps.values() + ] + } diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py index baef00b2596..303d0e91a36 100644 --- a/homeassistant/components/roku/coordinator.py +++ b/homeassistant/components/roku/coordinator.py @@ -62,10 +62,10 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): try: data = await self.roku.update(full_update=full_update) - - if full_update: - self.last_full_update = utcnow() - - return data except RokuError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error + + if full_update: + self.last_full_update = utcnow() + + return data diff --git a/homeassistant/components/romy/binary_sensor.py b/homeassistant/components/romy/binary_sensor.py new file mode 100644 index 00000000000..d8f6216007f --- /dev/null +++ b/homeassistant/components/romy/binary_sensor.py @@ -0,0 +1,73 @@ +"""Checking binary status values from your ROMY.""" + +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 RomyVacuumCoordinator +from .entity import RomyEntity + +BINARY_SENSORS: list[BinarySensorEntityDescription] = [ + BinarySensorEntityDescription( + key="dustbin", + translation_key="dustbin_present", + ), + BinarySensorEntityDescription( + key="dock", + translation_key="docked", + device_class=BinarySensorDeviceClass.PLUG, + ), + BinarySensorEntityDescription( + key="water_tank", + translation_key="water_tank_present", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BinarySensorEntityDescription( + key="water_tank_empty", + translation_key="water_tank_empty", + device_class=BinarySensorDeviceClass.PROBLEM, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ROMY vacuum cleaner.""" + + coordinator: RomyVacuumCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + RomyBinarySensor(coordinator, entity_description) + for entity_description in BINARY_SENSORS + if entity_description.key in coordinator.romy.binary_sensors + ) + + +class RomyBinarySensor(RomyEntity, BinarySensorEntity): + """RomyBinarySensor Class.""" + + entity_description: BinarySensorEntityDescription + + def __init__( + self, + coordinator: RomyVacuumCoordinator, + entity_description: BinarySensorEntityDescription, + ) -> None: + """Initialize the RomyBinarySensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entity_description.key}_{self.romy.unique_id}" + self.entity_description = entity_description + + @property + def is_on(self) -> bool: + """Return the value of the sensor.""" + return bool(self.romy.binary_sensors[self.entity_description.key]) diff --git a/homeassistant/components/romy/config_flow.py b/homeassistant/components/romy/config_flow.py index bccae667695..e571ff41c9a 100644 --- a/homeassistant/components/romy/config_flow.py +++ b/homeassistant/components/romy/config_flow.py @@ -5,15 +5,15 @@ from __future__ import annotations import romy 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 import homeassistant.helpers.config_validation as cv from .const import DOMAIN, LOGGER -class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RomyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle config flow for ROMY.""" VERSION = 1 @@ -26,7 +26,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> 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 - ) -> config_entries.ConfigFlowResult: + ) -> 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 - ) -> config_entries.ConfigFlowResult: + ) -> 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 - ) -> config_entries.ConfigFlowResult: + ) -> 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) -> config_entries.ConfigFlowResult: + async def _async_step_finish_config(self) -> ConfigFlowResult: """Finish the configuration setup.""" return self.async_create_entry( title=self.robot_name_given_by_user, diff --git a/homeassistant/components/romy/const.py b/homeassistant/components/romy/const.py index 5d42380902b..a41482ffe59 100644 --- a/homeassistant/components/romy/const.py +++ b/homeassistant/components/romy/const.py @@ -6,6 +6,6 @@ import logging from homeassistant.const import Platform DOMAIN = "romy" -PLATFORMS = [Platform.VACUUM] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.VACUUM] UPDATE_INTERVAL = timedelta(seconds=5) LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/romy/icons.json b/homeassistant/components/romy/icons.json new file mode 100644 index 00000000000..3425d5cfade --- /dev/null +++ b/homeassistant/components/romy/icons.json @@ -0,0 +1,37 @@ +{ + "entity": { + "binary_sensor": { + "water_tank_empty": { + "default": "mdi:cup-outline", + "state": { + "off": "mdi:cup-water", + "on": "mdi:cup-outline" + } + }, + "dustbin_present": { + "default": "mdi:basket-check", + "state": { + "off": "mdi:basket-remove", + "on": "mdi:basket-check" + } + } + }, + "sensor": { + "dustbin_sensor": { + "default": "mdi:basket-fill" + }, + "total_cleaning_time": { + "default": "mdi:clock" + }, + "total_number_of_cleaning_runs": { + "default": "mdi:counter" + }, + "total_area_cleaned": { + "default": "mdi:texture-box" + }, + "total_distance_driven": { + "default": "mdi:run" + } + } + } +} diff --git a/homeassistant/components/romy/sensor.py b/homeassistant/components/romy/sensor.py new file mode 100644 index 00000000000..bdd486c4f8f --- /dev/null +++ b/homeassistant/components/romy/sensor.py @@ -0,0 +1,112 @@ +"""Sensor checking adc and status values from your ROMY.""" + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + AREA_SQUARE_METERS, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, + UnitOfLength, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import RomyVacuumCoordinator +from .entity import RomyEntity + +SENSORS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="rssi", + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="dustbin_sensor", + translation_key="dustbin_sensor", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_cleaning_time", + translation_key="total_cleaning_time", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfTime.HOURS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_number_of_cleaning_runs", + translation_key="total_number_of_cleaning_runs", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="runs", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_area_cleaned", + translation_key="total_area_cleaned", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=AREA_SQUARE_METERS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_distance_driven", + translation_key="total_distance_driven", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfLength.METERS, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ROMY vacuum cleaner.""" + + coordinator: RomyVacuumCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + RomySensor(coordinator, entity_description) + for entity_description in SENSORS + if entity_description.key in coordinator.romy.sensors + ) + + +class RomySensor(RomyEntity, SensorEntity): + """RomySensor Class.""" + + entity_description: SensorEntityDescription + + def __init__( + self, + coordinator: RomyVacuumCoordinator, + entity_description: SensorEntityDescription, + ) -> None: + """Initialize ROMYs StatusSensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entity_description.key}_{self.romy.unique_id}" + self.entity_description = entity_description + + @property + def native_value(self) -> int: + """Return the value of the sensor.""" + value: int = self.romy.sensors[self.entity_description.key] + return value diff --git a/homeassistant/components/romy/strings.json b/homeassistant/components/romy/strings.json index 26dc60a2e84..78721da17ba 100644 --- a/homeassistant/components/romy/strings.json +++ b/homeassistant/components/romy/strings.json @@ -46,6 +46,37 @@ } } } + }, + "binary_sensor": { + "dustbin_present": { + "name": "Dustbin present" + }, + "docked": { + "name": "Robot docked" + }, + "water_tank_present": { + "name": "Watertank present" + }, + "water_tank_empty": { + "name": "Watertank empty" + } + }, + "sensor": { + "dustbin_sensor": { + "name": "Dustbin dirt level" + }, + "total_cleaning_time": { + "name": "Total cleaning time" + }, + "total_number_of_cleaning_runs": { + "name": "Total cleaning runs" + }, + "total_area_cleaned": { + "name": "Total cleaned area" + }, + "total_distance_driven": { + "name": "Total distance driven" + } } } } diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 7b834421135..53ea9aa7c44 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -4,8 +4,9 @@ from __future__ import annotations import asyncio from functools import partial +from typing import Any -from roombapy import RoombaFactory +from roombapy import RoombaFactory, RoombaInfo from roombapy.discovery import RoombaDiscovery from roombapy.getpassword import RoombaPassword import voluptuous as vol @@ -15,7 +16,7 @@ from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, - OptionsFlow, + OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback @@ -43,7 +44,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: HomeAssistant, data): +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. @@ -75,20 +76,21 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + name: str | None = None + blid: str | None = None + host: str | None = None + + def __init__(self) -> None: """Initialize the roomba flow.""" - self.discovered_robots = {} - self.name = None - self.blid = None - self.host = None + self.discovered_robots: dict[str, RoombaInfo] = {} @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, - ) -> OptionsFlowHandler: + ) -> RoombaOptionsFlowHandler: """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) + return RoombaOptionsFlowHandler(config_entry) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo @@ -135,8 +137,9 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"host": self.host, "name": self.blid} return await self.async_step_user() - async def _async_start_link(self): + async def _async_start_link(self) -> ConfigFlowResult: """Start linking.""" + assert self.host device = self.discovered_robots[self.host] self.blid = device.blid self.name = device.robot_name @@ -144,7 +147,9 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_link() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle a flow start.""" # Check if user chooses manual entry if user_input is not None and not user_input.get(CONF_HOST): @@ -181,25 +186,23 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): if not self.discovered_robots: return await self.async_step_manual() + hosts: dict[str | None, str] = { + **{ + device.ip: f"{device.robot_name} ({device.ip})" + for device in devices + if device.blid not in already_configured + }, + None: "Manually add a Roomba or Braava", + } + return self.async_show_form( step_id="user", - data_schema=vol.Schema( - { - vol.Optional("host"): vol.In( - { - **{ - device.ip: f"{device.robot_name} ({device.ip})" - for device in devices - if device.blid not in already_configured - }, - None: "Manually add a Roomba or Braava", - } - ) - } - ), + data_schema=vol.Schema({vol.Optional("host"): vol.In(hosts)}), ) - async def async_step_manual(self, user_input=None): + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle manual device setup.""" if user_input is None: return self.async_show_form( @@ -224,7 +227,9 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_link() - async def async_step_link(self, user_input=None): + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Attempt to link with the Roomba. Given a configured host, will ask the user to press the home and target buttons @@ -235,7 +240,7 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): step_id="link", description_placeholders={CONF_NAME: self.name or self.blid}, ) - + assert self.host roomba_pw = RoombaPassword(self.host) try: @@ -260,10 +265,12 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") self.name = info[CONF_NAME] - + assert self.name return self.async_create_entry(title=self.name, data=config) - async def async_step_link_manual(self, user_input=None): + async def async_step_link_manual( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle manual linking.""" errors = {} @@ -278,8 +285,7 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, config) except CannotConnect: errors = {"base": "cannot_connect"} - - if not errors: + else: return self.async_create_entry(title=info[CONF_NAME], data=config) return self.async_show_form( @@ -290,14 +296,12 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(OptionsFlow): +class RoombaOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle options.""" - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -308,15 +312,11 @@ class OptionsFlowHandler(OptionsFlow): { vol.Optional( CONF_CONTINUOUS, - default=self.config_entry.options.get( - CONF_CONTINUOUS, DEFAULT_CONTINUOUS - ), + default=self.options.get(CONF_CONTINUOUS, DEFAULT_CONTINUOUS), ): bool, vol.Optional( CONF_DELAY, - default=self.config_entry.options.get( - CONF_DELAY, DEFAULT_DELAY - ), + default=self.options.get(CONF_DELAY, DEFAULT_DELAY), ): int, } ), @@ -324,7 +324,7 @@ class OptionsFlowHandler(OptionsFlow): @callback -def _async_get_roomba_discovery(): +def _async_get_roomba_discovery() -> RoombaDiscovery: """Create a discovery object.""" discovery = RoombaDiscovery() discovery.amount_of_broadcasted_messages = MAX_NUM_DEVICES_TO_DISCOVER @@ -332,24 +332,28 @@ def _async_get_roomba_discovery(): @callback -def _async_blid_from_hostname(hostname): +def _async_blid_from_hostname(hostname: str) -> str: """Extract the blid from the hostname.""" return hostname.split("-")[1].split(".")[0].upper() -async def _async_discover_roombas(hass, host): - discovered_hosts = set() - devices = [] +async def _async_discover_roombas( + hass: HomeAssistant, host: str | None = None +) -> list[RoombaInfo]: + discovered_hosts: set[str] = set() + devices: list[RoombaInfo] = [] discover_lock = hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock()) discover_attempts = HOST_ATTEMPTS if host else ALL_ATTEMPTS for attempt in range(discover_attempts + 1): async with discover_lock: discovery = _async_get_roomba_discovery() + discovered: set[RoombaInfo] = set() try: if host: device = await hass.async_add_executor_job(discovery.get, host) - discovered = [device] if device else [] + if device: + discovered.add(device) else: discovered = await hass.async_add_executor_job(discovery.get_all) except OSError: diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index d24cdb0c98d..2dc0bf71cd4 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -97,9 +97,7 @@ async def discover(hass): """Connect and authenticate home assistant.""" hub = RoonHub(hass) - servers = await hub.discover() - - return servers + return await hub.discover() async def authenticate(hass: HomeAssistant, host, port, servers): diff --git a/homeassistant/components/roon/event.py b/homeassistant/components/roon/event.py index ea5014c8755..073b58160f6 100644 --- a/homeassistant/components/roon/event.py +++ b/homeassistant/components/roon/event.py @@ -72,7 +72,6 @@ class RoonEventEntity(EventEntity): via_device=(DOMAIN, self._server.roon_id), ) - @callback def _roonapi_volume_callback( self, control_key: str, event: str, value: int ) -> None: @@ -88,7 +87,7 @@ class RoonEventEntity(EventEntity): event = "volume_down" self._trigger_event(event) - self.async_write_ha_state() + self.schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Register volume hooks with the roon api.""" diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 3db91f7926f..d4ce0d2cc97 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/route53", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], - "requirements": ["boto3==1.33.13"] + "requirements": ["boto3==1.34.51"] } diff --git a/homeassistant/components/rova/config_flow.py b/homeassistant/components/rova/config_flow.py index d618681783e..e5e3a31b8af 100644 --- a/homeassistant/components/rova/config_flow.py +++ b/homeassistant/components/rova/config_flow.py @@ -6,19 +6,19 @@ from requests.exceptions import ConnectTimeout, HTTPError from rova.rova import Rova import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, DOMAIN -class RovaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RovaConfigFlow(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: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" errors: dict[str, str] = {} @@ -60,9 +60,7 @@ class RovaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import( - self, user_input: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" zip_code = user_input[CONF_ZIP_CODE] number = user_input[CONF_HOUSE_NUMBER] diff --git a/homeassistant/components/ruuvitag_ble/strings.json b/homeassistant/components/ruuvitag_ble/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/ruuvitag_ble/strings.json +++ b/homeassistant/components/ruuvitag_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/rympro/coordinator.py b/homeassistant/components/rympro/coordinator.py index f2e5162a0f0..19f16005578 100644 --- a/homeassistant/components/rympro/coordinator.py +++ b/homeassistant/components/rympro/coordinator.py @@ -39,10 +39,10 @@ class RymProDataUpdateCoordinator(DataUpdateCoordinator[dict[int, dict]]): meter["consumption_forecast"] = await self.rympro.consumption_forecast( meter_id ) - return meters except UnauthorizedError as error: assert self.config_entry await self.hass.config_entries.async_reload(self.config_entry.entry_id) raise UpdateFailed(error) from error except (CannotConnectError, OperationError) as error: raise UpdateFailed(error) from error + return meters diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 6a68f98203b..ebb9284a7f2 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -6,7 +6,6 @@ from collections.abc import Callable, Coroutine import logging from typing import Any -from pysabnzbd import SabnzbdApiException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState @@ -23,9 +22,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import async_get -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( @@ -37,15 +34,11 @@ from .const import ( DEFAULT_SPEED_LIMIT, DEFAULT_SSL, DOMAIN, - KEY_API, - KEY_API_DATA, - KEY_NAME, SERVICE_PAUSE, SERVICE_RESUME, SERVICE_SET_SPEED, - SIGNAL_SABNZBD_UPDATED, - UPDATE_INTERVAL, ) +from .coordinator import SabnzbdUpdateCoordinator from .sab import get_client from .sensor import OLD_SENSOR_KEYS @@ -179,30 +172,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not sab_api: raise ConfigEntryNotReady - sab_api_data = SabnzbdApiData(sab_api) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - KEY_API: sab_api, - KEY_API_DATA: sab_api_data, - KEY_NAME: entry.data[CONF_NAME], - } - await migrate_unique_id(hass, entry) update_device_identifiers(hass, entry) + coordinator = SabnzbdUpdateCoordinator(hass, sab_api) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + @callback def extract_api( - func: Callable[[ServiceCall, SabnzbdApiData], Coroutine[Any, Any, None]], + func: Callable[ + [ServiceCall, SabnzbdUpdateCoordinator], Coroutine[Any, Any, None] + ], ) -> Callable[[ServiceCall], Coroutine[Any, Any, None]]: """Define a decorator to get the correct api for a service call.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) - api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] + coordinator: SabnzbdUpdateCoordinator = hass.data[DOMAIN][entry_id] try: - await func(call, api_data) + await func(call, coordinator) except Exception as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -211,17 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper @extract_api - async def async_pause_queue(call: ServiceCall, api: SabnzbdApiData) -> None: - await api.async_pause_queue() + async def async_pause_queue( + call: ServiceCall, coordinator: SabnzbdUpdateCoordinator + ) -> None: + await coordinator.sab_api.pause_queue() @extract_api - async def async_resume_queue(call: ServiceCall, api: SabnzbdApiData) -> None: - await api.async_resume_queue() + async def async_resume_queue( + call: ServiceCall, coordinator: SabnzbdUpdateCoordinator + ) -> None: + await coordinator.sab_api.resume_queue() @extract_api - async def async_set_queue_speed(call: ServiceCall, api: SabnzbdApiData) -> None: + async def async_set_queue_speed( + call: ServiceCall, coordinator: SabnzbdUpdateCoordinator + ) -> None: speed = call.data.get(ATTR_SPEED) - await api.async_set_queue_speed(speed) + await coordinator.sab_api.set_speed_limit(speed) for service, method, schema in ( (SERVICE_PAUSE, async_pause_queue, SERVICE_BASE_SCHEMA), @@ -233,18 +230,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_register(DOMAIN, service, method, schema=schema) - async def async_update_sabnzbd(now): - """Refresh SABnzbd queue data.""" - try: - await sab_api.refresh_data() - async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) - except SabnzbdApiException as err: - _LOGGER.error(err) - - entry.async_on_unload( - async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) - ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -268,42 +253,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, service_name) return unload_ok - - -class SabnzbdApiData: - """Class for storing/refreshing sabnzbd api queue data.""" - - def __init__(self, sab_api): - """Initialize component.""" - self.sab_api = sab_api - - async def async_pause_queue(self): - """Pause Sabnzbd queue.""" - - try: - return await self.sab_api.pause_queue() - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - async def async_resume_queue(self): - """Resume Sabnzbd queue.""" - - try: - return await self.sab_api.resume_queue() - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - async def async_set_queue_speed(self, limit): - """Set speed limit for the Sabnzbd queue.""" - - try: - return await self.sab_api.set_speed_limit(limit) - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - def get_queue_field(self, field): - """Return the value for the given field from the Sabnzbd queue.""" - return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sabnzbd/const.py b/homeassistant/components/sabnzbd/const.py index a9cd80898f7..55346509133 100644 --- a/homeassistant/components/sabnzbd/const.py +++ b/homeassistant/components/sabnzbd/const.py @@ -1,7 +1,5 @@ """Constants for the Sabnzbd component.""" -from datetime import timedelta - DOMAIN = "sabnzbd" DATA_SABNZBD = "sabnzbd" @@ -14,14 +12,6 @@ DEFAULT_PORT = 8080 DEFAULT_SPEED_LIMIT = "100" DEFAULT_SSL = False -UPDATE_INTERVAL = timedelta(seconds=30) - SERVICE_PAUSE = "pause" SERVICE_RESUME = "resume" SERVICE_SET_SPEED = "set_speed" - -SIGNAL_SABNZBD_UPDATED = "sabnzbd_updated" - -KEY_API = "api" -KEY_API_DATA = "api_data" -KEY_NAME = "name" diff --git a/homeassistant/components/sabnzbd/coordinator.py b/homeassistant/components/sabnzbd/coordinator.py new file mode 100644 index 00000000000..5db59bb584b --- /dev/null +++ b/homeassistant/components/sabnzbd/coordinator.py @@ -0,0 +1,40 @@ +"""DataUpdateCoordinator for the SABnzbd integration.""" + +from datetime import timedelta +import logging +from typing import Any + +from pysabnzbd import SabnzbdApi, SabnzbdApiException + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + + +class SabnzbdUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """The SABnzbd update coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + sab_api: SabnzbdApi, + ) -> None: + """Initialize the SABnzbd update coordinator.""" + self.sab_api = sab_api + + super().__init__( + hass, + _LOGGER, + name="SABnzbd", + update_interval=timedelta(seconds=30), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Get the latest data from the SABnzbd API.""" + try: + await self.sab_api.refresh_data() + except SabnzbdApiException as err: + raise UpdateFailed("Error while fetching data") from err + + return self.sab_api.queue diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 1fb0d09dd60..afc35a2340e 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -1,7 +1,7 @@ { "domain": "sabnzbd", "name": "SABnzbd", - "codeowners": ["@shaiu"], + "codeowners": ["@shaiu", "@jpbede"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "iot_class": "local_polling", diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index d5f19b5e718..d956d06f1ac 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -14,11 +14,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, SIGNAL_SABNZBD_UPDATED -from .const import DEFAULT_NAME, KEY_API_DATA +from . import DOMAIN, SabnzbdUpdateCoordinator +from .const import DEFAULT_NAME @dataclass(frozen=True, kw_only=True) @@ -28,18 +29,18 @@ class SabnzbdSensorEntityDescription(SensorEntityDescription): key: str -SPEED_KEY = "kbpersec" - SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( SabnzbdSensorEntityDescription( key="status", translation_key="status", ), SabnzbdSensorEntityDescription( - key=SPEED_KEY, + key="kbpersec", translation_key="speed", device_class=SensorDeviceClass.DATA_RATE, - native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( @@ -74,6 +75,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( key="noofslots_total", translation_key="queue_count", state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="day_size", @@ -82,6 +84,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="week_size", @@ -90,6 +93,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="month_size", @@ -98,6 +102,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="total_size", @@ -105,6 +110,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), ) @@ -131,15 +137,14 @@ async def async_setup_entry( """Set up a Sabnzbd sensor entry.""" entry_id = config_entry.entry_id - - sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] + coordinator: SabnzbdUpdateCoordinator = hass.data[DOMAIN][entry_id] async_add_entities( - [SabnzbdSensor(sab_api_data, sensor, entry_id) for sensor in SENSOR_TYPES] + [SabnzbdSensor(coordinator, sensor, entry_id) for sensor in SENSOR_TYPES] ) -class SabnzbdSensor(SensorEntity): +class SabnzbdSensor(CoordinatorEntity[SabnzbdUpdateCoordinator], SensorEntity): """Representation of an SABnzbd sensor.""" entity_description: SabnzbdSensorEntityDescription @@ -148,40 +153,22 @@ class SabnzbdSensor(SensorEntity): def __init__( self, - sabnzbd_api_data, + coordinator: SabnzbdUpdateCoordinator, description: SabnzbdSensorEntityDescription, entry_id, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self._attr_unique_id = f"{entry_id}_{description.key}" self.entity_description = description - self._sabnzbd_api = sabnzbd_api_data self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, name=DEFAULT_NAME, ) - async def async_added_to_hass(self) -> None: - """Call when entity about to be added to hass.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_SABNZBD_UPDATED, self.update_state - ) - ) - - def update_state(self, args): - """Get the latest data and updates the states.""" - self._attr_native_value = self._sabnzbd_api.get_queue_field( - self.entity_description.key - ) - - if self._attr_native_value is not None: - if self.entity_description.key == SPEED_KEY: - self._attr_native_value = round( - float(self._attr_native_value) / 1024, 1 - ) - elif "size" in self.entity_description.key: - self._attr_native_value = round(float(self._attr_native_value), 2) - self.schedule_update_ha_state() + @property + def native_value(self) -> StateType: + """Return latest sensor data.""" + return self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 68a58710c19..9dcb2f9f57e 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -149,9 +149,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) ) await _async_update_ssdp_locations(hass, entry) diff --git a/homeassistant/components/samsungtv/device_trigger.py b/homeassistant/components/samsungtv/device_trigger.py index e47cde785eb..5b8ff3ebdb8 100644 --- a/homeassistant/components/samsungtv/device_trigger.py +++ b/homeassistant/components/samsungtv/device_trigger.py @@ -55,8 +55,7 @@ async def async_get_triggers( _hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for device.""" - triggers = [async_get_turn_on_trigger(device_id)] - return triggers + return [async_get_turn_on_trigger(device_id)] async def async_attach_trigger( diff --git a/homeassistant/components/sanix/__init__.py b/homeassistant/components/sanix/__init__.py new file mode 100644 index 00000000000..c8c5567eedc --- /dev/null +++ b/homeassistant/components/sanix/__init__.py @@ -0,0 +1,37 @@ +"""The Sanix integration.""" + +from sanix import Sanix + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant + +from .const import CONF_SERIAL_NUMBER, DOMAIN +from .coordinator import SanixCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Sanix from a config entry.""" + + serial_no = entry.data[CONF_SERIAL_NUMBER] + token = entry.data[CONF_TOKEN] + + sanix_api = Sanix(serial_no, token) + coordinator = SanixCoordinator(hass, sanix_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 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/sanix/config_flow.py b/homeassistant/components/sanix/config_flow.py new file mode 100644 index 00000000000..57aa5a5293a --- /dev/null +++ b/homeassistant/components/sanix/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for Sanix integration.""" + +import logging +from typing import Any + +from sanix import Sanix +from sanix.exceptions import SanixException, SanixInvalidAuthException +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_TOKEN + +from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_SERIAL_NUMBER): str, + vol.Required(CONF_TOKEN): str, + } +) + + +class SanixConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Sanix.""" + + 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 user_input: + await self.async_set_unique_id(user_input[CONF_SERIAL_NUMBER]) + self._abort_if_unique_id_configured() + + sanix_api = Sanix(user_input[CONF_SERIAL_NUMBER], user_input[CONF_TOKEN]) + + try: + await self.hass.async_add_executor_job(sanix_api.fetch_data) + except SanixInvalidAuthException: + errors["base"] = "invalid_auth" + except SanixException: + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=MANUFACTURER, + data=user_input, + ) + + return self.async_show_form( + step_id="user", + description_placeholders={"dashboard_url": "https://sanix.bitcomplex.pl/"}, + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/sanix/const.py b/homeassistant/components/sanix/const.py new file mode 100644 index 00000000000..22ab33823d6 --- /dev/null +++ b/homeassistant/components/sanix/const.py @@ -0,0 +1,8 @@ +"""Constants for the Sanix integration.""" + +CONF_SERIAL_NUMBER = "serial_number" + +DOMAIN = "sanix" +MANUFACTURER = "Sanix" + +SANIX_API_HOST = "https://sanix.bitcomplex.pl" diff --git a/homeassistant/components/sanix/coordinator.py b/homeassistant/components/sanix/coordinator.py new file mode 100644 index 00000000000..d6362337a38 --- /dev/null +++ b/homeassistant/components/sanix/coordinator.py @@ -0,0 +1,36 @@ +"""Sanix Coordinator.""" + +from datetime import timedelta +import logging + +from sanix import Sanix +from sanix.exceptions import SanixException +from sanix.models import Measurement + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +class SanixCoordinator(DataUpdateCoordinator[Measurement]): + """Sanix coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, sanix_api: Sanix) -> None: + """Initialize coordinator.""" + super().__init__( + hass, _LOGGER, name=MANUFACTURER, update_interval=timedelta(hours=1) + ) + self._sanix_api = sanix_api + + async def _async_update_data(self) -> Measurement: + """Fetch data from API endpoint.""" + try: + return await self.hass.async_add_executor_job(self._sanix_api.fetch_data) + except SanixException as err: + raise UpdateFailed("Error while communicating with the API") from err diff --git a/homeassistant/components/sanix/icons.json b/homeassistant/components/sanix/icons.json new file mode 100644 index 00000000000..2b49cf8ea20 --- /dev/null +++ b/homeassistant/components/sanix/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "fill_perc": { + "default": "mdi:water-percent" + } + } + } +} diff --git a/homeassistant/components/sanix/manifest.json b/homeassistant/components/sanix/manifest.json new file mode 100644 index 00000000000..4e1c6d56add --- /dev/null +++ b/homeassistant/components/sanix/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "sanix", + "name": "Sanix", + "codeowners": ["@tomaszsluszniak"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sanix", + "iot_class": "cloud_polling", + "requirements": ["sanix==1.0.5"] +} diff --git a/homeassistant/components/sanix/sensor.py b/homeassistant/components/sanix/sensor.py new file mode 100644 index 00000000000..39a1c593433 --- /dev/null +++ b/homeassistant/components/sanix/sensor.py @@ -0,0 +1,123 @@ +"""Platform for Sanix integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import date, datetime + +from sanix.const import ( + ATTR_API_BATTERY, + ATTR_API_DEVICE_NO, + ATTR_API_DISTANCE, + ATTR_API_FILL_PERC, + ATTR_API_SERVICE_DATE, + ATTR_API_SSID, +) +from sanix.models import Measurement + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfLength +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import SanixCoordinator + + +@dataclass(frozen=True, kw_only=True) +class SanixSensorEntityDescription(SensorEntityDescription): + """Class describing Sanix Sensor entities.""" + + native_value_fn: Callable[[Measurement], int | datetime | date | str] + + +SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = ( + SanixSensorEntityDescription( + key=ATTR_API_BATTERY, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + native_value_fn=lambda data: data.battery, + ), + SanixSensorEntityDescription( + key=ATTR_API_DISTANCE, + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + native_value_fn=lambda data: data.distance, + ), + SanixSensorEntityDescription( + key=ATTR_API_SERVICE_DATE, + translation_key=ATTR_API_SERVICE_DATE, + device_class=SensorDeviceClass.DATE, + native_value_fn=lambda data: data.service_date, + ), + SanixSensorEntityDescription( + key=ATTR_API_FILL_PERC, + translation_key=ATTR_API_FILL_PERC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_value_fn=lambda data: data.fill_perc, + ), + SanixSensorEntityDescription( + key=ATTR_API_SSID, + translation_key=ATTR_API_SSID, + entity_registry_enabled_default=False, + native_value_fn=lambda data: data.ssid, + ), + SanixSensorEntityDescription( + key=ATTR_API_DEVICE_NO, + translation_key=ATTR_API_DEVICE_NO, + entity_registry_enabled_default=False, + native_value_fn=lambda data: data.device_no, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sanix Sensor entities based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SanixSensorEntity(coordinator, description) for description in SENSOR_TYPES + ) + + +class SanixSensorEntity(CoordinatorEntity[SanixCoordinator], SensorEntity): + """Sanix Sensor entity.""" + + _attr_has_entity_name = True + entity_description: SanixSensorEntityDescription + + def __init__( + self, + coordinator: SanixCoordinator, + description: SanixSensorEntityDescription, + ) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + serial_no = str(coordinator.config_entry.unique_id) + + self._attr_unique_id = f"{serial_no}-{description.key}" + self.entity_description = description + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_no)}, + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + serial_number=serial_no, + ) + + @property + def native_value(self) -> int | datetime | date | str: + """Return the state of the sensor.""" + return self.entity_description.native_value_fn(self.coordinator.data) diff --git a/homeassistant/components/sanix/strings.json b/homeassistant/components/sanix/strings.json new file mode 100644 index 00000000000..6bff11e36af --- /dev/null +++ b/homeassistant/components/sanix/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "description": "To get the Serial number and the Token you just have to sign in to the [Sanix Dashboard]({dashboard_url}) and open the Help -> System version page.", + "data": { + "serial_number": "Serial number", + "token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "service_date": { + "name": "Service date" + }, + "fill_perc": { + "name": "Filled" + }, + "device_no": { + "name": "Device number" + }, + "ssid": { + "name": "SSID" + } + } + } +} diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 2dc2ff2d035..e69a6761bc7 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -256,8 +256,7 @@ class Schedule(CollectionEntity): @classmethod def from_storage(cls, config: ConfigType) -> Schedule: """Return entity instance initialized from storage.""" - schedule = cls(config, editable=True) - return schedule + return cls(config, editable=True) @classmethod def from_yaml(cls, config: ConfigType) -> Schedule: diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 217e69b27df..9b534aed77b 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -21,13 +21,13 @@ "encoding": "Character encoding" }, "data_description": { - "resource": "The URL to the website that contains the value", - "authentication": "Type of the HTTP authentication. Either basic or digest", - "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", - "headers": "Headers to use for the web request", - "timeout": "Timeout for connection to website", - "encoding": "Character encoding to use. Defaults to UTF-8", - "payload": "Payload to use when method is POST" + "resource": "The URL to the website that contains the value.", + "authentication": "Type of the HTTP authentication. Either basic or digest.", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed.", + "headers": "Headers to use for the web request.", + "timeout": "Timeout for connection to website.", + "encoding": "Character encoding to use. Defaults to UTF-8.", + "payload": "Payload to use when method is POST." } }, "sensor": { @@ -36,19 +36,21 @@ "attribute": "Attribute", "index": "Index", "select": "Select", - "value_template": "Value Template", - "device_class": "Device Class", - "state_class": "State Class", - "unit_of_measurement": "Unit of Measurement" + "value_template": "Value template", + "availability": "Availability template", + "device_class": "Device class", + "state_class": "State class", + "unit_of_measurement": "Unit of measurement" }, "data_description": { - "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", - "attribute": "Get value of an attribute on the selected tag", - "index": "Defines which of the elements returned by the CSS selector to use", - "value_template": "Defines a template to get the state of the sensor", - "device_class": "The type/class of the sensor to set the icon in the frontend", - "state_class": "The state_class of the sensor", - "unit_of_measurement": "Choose temperature measurement or create your own" + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details.", + "attribute": "Get value of an attribute on the selected tag.", + "index": "Defines which of the elements returned by the CSS selector to use.", + "value_template": "Defines a template to get the state of the sensor.", + "availability": "Defines a template to get the availability of the sensor.", + "device_class": "The type/class of the sensor to set the icon in the frontend.", + "state_class": "The state_class of the sensor.", + "unit_of_measurement": "Choose unit of measurement or create your own." } } } @@ -70,6 +72,7 @@ "index": "[%key:component::scrape::config::step::sensor::data::index%]", "select": "[%key:component::scrape::config::step::sensor::data::select%]", "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" @@ -79,6 +82,7 @@ "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data_description::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" @@ -91,6 +95,7 @@ "index": "[%key:component::scrape::config::step::sensor::data::index%]", "select": "[%key:component::scrape::config::step::sensor::data::select%]", "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" @@ -100,6 +105,7 @@ "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data_description::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 82752ed15bc..f83aed68590 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio from dataclasses import dataclass +from functools import cached_property import logging from typing import TYPE_CHECKING, Any, cast @@ -38,7 +39,6 @@ from homeassistant.core import ( SupportsResponse, callback, ) -from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -74,12 +74,6 @@ from .const import ( from .helpers import async_get_blueprints from .trace import trace_script -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES): {str: cv.match_all}} @@ -464,15 +458,10 @@ class UnavailableScriptEntity(BaseScriptEntity): raw_config: ConfigType | None, ) -> None: """Initialize a script entity.""" - self._name = raw_config.get(CONF_ALIAS, key) if raw_config else key + self._attr_name = raw_config.get(CONF_ALIAS, key) if raw_config else key self._attr_unique_id = key self.raw_config = raw_config - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @cached_property def referenced_labels(self) -> set[str]: """Return a set of referenced labels.""" @@ -508,6 +497,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): """Representation of a script entity.""" icon = None + _attr_should_poll = False def __init__(self, hass, key, cfg, raw_config, blueprint_inputs): """Initialize the script.""" @@ -536,29 +526,21 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): self.raw_config = raw_config self._trace_config = cfg[CONF_TRACE] self._blueprint_inputs = blueprint_inputs - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the entity.""" - return self.script.name + self._attr_name = self.script.name @property def extra_state_attributes(self): """Return the state attributes.""" + script = self.script attrs = { - ATTR_LAST_TRIGGERED: self.script.last_triggered, - ATTR_MODE: self.script.script_mode, - ATTR_CUR: self.script.runs, + ATTR_LAST_TRIGGERED: script.last_triggered, + ATTR_MODE: script.script_mode, + ATTR_CUR: script.runs, } - if self.script.supports_max: - attrs[ATTR_MAX] = self.script.max_runs - if self.script.last_action: - attrs[ATTR_LAST_ACTION] = self.script.last_action + if script.supports_max: + attrs[ATTR_MAX] = script.max_runs + if script.last_action: + attrs[ATTR_LAST_ACTION] = script.last_action return attrs @property @@ -678,9 +660,13 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Restore last triggered on startup and register service.""" + if TYPE_CHECKING: + assert self.unique_id is not None + assert self.registry_entry is not None - unique_id = cast(str, self.unique_id) - self.hass.services.async_register( + unique_id = self.unique_id + hass = self.hass + hass.services.async_register( DOMAIN, unique_id, self._service_handler, @@ -690,15 +676,16 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): # Register the service description service_desc = { - CONF_NAME: cast(er.RegistryEntry, self.registry_entry).name or self.name, + CONF_NAME: self.registry_entry.name or self.name, CONF_DESCRIPTION: self.description, CONF_FIELDS: self.fields, } - async_set_service_schema(self.hass, DOMAIN, unique_id, service_desc) + async_set_service_schema(hass, DOMAIN, unique_id, service_desc) - if state := await self.async_get_last_state(): - if last_triggered := state.attributes.get("last_triggered"): - self.script.last_triggered = parse_datetime(last_triggered) + if (state := await self.async_get_last_state()) and ( + last_triggered := state.attributes.get("last_triggered") + ): + self.script.last_triggered = parse_datetime(last_triggered) async def async_will_remove_from_hass(self): """Stop script and remove service when it will be removed from HA.""" diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index a50cda752d0..0013f1411dd 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -40,7 +40,7 @@ def trace_script( except Exception as ex: if item_id: trace.set_error(ex) - raise ex + raise finally: if item_id: trace.finished() diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 0c54dfc0aac..6e134c8958c 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -32,11 +33,6 @@ from .const import ( SERVICE_SELECT_PREVIOUS, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 0a2b23b2cd9..81ab3a06067 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -311,8 +311,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return value of sensor.""" - state = self.entity_description.value_fn(self.device_data) - return state + return self.entity_description.value_fn(self.device_data) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: diff --git a/homeassistant/components/sensirion_ble/strings.json b/homeassistant/components/sensirion_ble/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/sensirion_ble/strings.json +++ b/homeassistant/components/sensirion_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 92499a05af4..a955e861c20 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,10 +8,10 @@ from contextlib import suppress from dataclasses import dataclass from datetime import UTC, date, datetime, timedelta from decimal import Decimal, InvalidOperation as DecimalInvalidOperation -from functools import partial +from functools import cached_property, partial import logging from math import ceil, floor, isfinite, log10 -from typing import TYPE_CHECKING, Any, Final, Self, cast, final, override +from typing import Any, Final, Self, cast, final, override from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 @@ -91,11 +91,6 @@ from .const import ( # noqa: F401 ) from .websocket_api import async_setup as async_setup_ws_api -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" @@ -752,13 +747,15 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return value - def _suggested_precision_or_none(self) -> int | None: - """Return suggested display precision, or None if not set.""" + def _display_precision_or_none(self) -> int | None: + """Return display precision, or None if not set.""" assert self.registry_entry - if (sensor_options := self.registry_entry.options.get(DOMAIN)) and ( - precision := sensor_options.get("suggested_display_precision") - ) is not None: - return cast(int, precision) + if not (sensor_options := self.registry_entry.options.get(DOMAIN)): + return None + + for option in ("display_precision", "suggested_display_precision"): + if (precision := sensor_options.get(option)) is not None: + return cast(int, precision) return None def _update_suggested_precision(self) -> None: @@ -789,11 +786,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log) display_precision = max(0, display_precision + ratio_log) - if display_precision is None and ( - DOMAIN not in self.registry_entry.options - or "suggested_display_precision" not in self.registry_entry.options - ): - return sensor_options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {}) if ( "suggested_display_precision" in sensor_options @@ -840,7 +832,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): Called when the entity registry entry has been updated and before the sensor is added to the state machine. """ - self._sensor_option_display_precision = self._suggested_precision_or_none() + self._sensor_option_display_precision = self._display_precision_or_none() assert self.registry_entry if ( sensor_options := self.registry_entry.options.get(f"{DOMAIN}.private") diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 97ad49fb937..26bb4f4376b 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Callable, Iterable import datetime import itertools import logging @@ -402,7 +402,7 @@ def compile_statistics( # noqa: C901 entities_full_history = [ i.entity_id for i in sensor_states if "sum" in wanted_statistics[i.entity_id] ] - history_list: MutableMapping[str, list[State]] = {} + history_list: dict[str, list[State]] = {} if entities_full_history: history_list = history.get_full_significant_states_with_session( hass, @@ -511,9 +511,13 @@ def compile_statistics( # noqa: C901 # Make calculations stat: StatisticData = {"start": start} if "max" in wanted_statistics[entity_id]: - stat["max"] = max(*itertools.islice(zip(*valid_float_states), 1)) + stat["max"] = max( + *itertools.islice(zip(*valid_float_states, strict=False), 1) + ) if "min" in wanted_statistics[entity_id]: - stat["min"] = min(*itertools.islice(zip(*valid_float_states), 1)) + stat["min"] = min( + *itertools.islice(zip(*valid_float_states, strict=False), 1) + ) if "mean" in wanted_statistics[entity_id]: stat["mean"] = _time_weighted_average(valid_float_states, start, end) diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index f320a7efcdf..00de36fc67c 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -20,10 +20,10 @@ from . import SensorDeviceClass def _absolute_and_relative_change( - old_state: int | float | None, - new_state: int | float | None, - absolute_change: int | float, - percentage_change: int | float, + old_state: float | None, + new_state: float | None, + absolute_change: float, + percentage_change: float, ) -> bool: return check_absolute_change( old_state, new_state, absolute_change diff --git a/homeassistant/components/sensorpro/strings.json b/homeassistant/components/sensorpro/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/sensorpro/strings.json +++ b/homeassistant/components/sensorpro/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/sensorpush/strings.json b/homeassistant/components/sensorpush/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/sensorpush/strings.json +++ b/homeassistant/components/sensorpush/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 7f40279df85..5f2b1ea3c3c 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -187,12 +187,10 @@ class SerialSensor(SensorEntity): **kwargs, ) - except SerialException as exc: + except SerialException: if not logged_error: _LOGGER.exception( - "Unable to connect to the serial device %s: %s. Will retry", - device, - exc, + "Unable to connect to the serial device %s. Will retry", device ) logged_error = True await self._handle_error() @@ -201,9 +199,9 @@ class SerialSensor(SensorEntity): while True: try: line = await reader.readline() - except SerialException as exc: + except SerialException: _LOGGER.exception( - "Error while reading serial device %s: %s", device, exc + "Error while reading serial device %s", device ) await self._handle_error() break diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 183d1bd4068..40c9c8d58d1 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -4,14 +4,80 @@ 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.const import ( + ATTR_FRIENDLY_NAME, + ATTR_LOCATION, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify -from .const import DOMAIN +from .const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_INFO_TEXT, + ATTR_PACKAGE_STATE, + ATTR_STATUS, + ATTR_TIMESTAMP, + ATTR_TRACKING_NUMBER, + DOMAIN, + SERVICE_GET_PACKAGES, +) +from .coordinator import SeventeenTrackCoordinator -PLATFORMS = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.SENSOR] + +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the 17Track component.""" + + async def get_packages(call: ServiceCall) -> ServiceResponse: + """Get packages from 17Track.""" + config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + package_states = call.data.get(ATTR_PACKAGE_STATE, []) + seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][ + config_entry_id + ] + live_packages = sorted( + await seventeen_coordinator.client.profile.packages( + show_archived=seventeen_coordinator.show_archived + ) + ) + + return { + "packages": [ + { + ATTR_TRACKING_NUMBER: package.tracking_number, + ATTR_LOCATION: package.location, + ATTR_STATUS: package.status, + ATTR_TIMESTAMP: package.timestamp, + ATTR_INFO_TEXT: package.info_text, + ATTR_FRIENDLY_NAME: package.friendly_name, + } + for package in live_packages + if slugify(package.status) in package_states or package_states == [] + ] + } + + hass.services.async_register( + DOMAIN, + SERVICE_GET_PACKAGES, + get_packages, + supports_response=SupportsResponse.ONLY, + ) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -25,8 +91,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SeventeenTrackError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + seventeen_coordinator = SeventeenTrackCoordinator(hass, client) + await seventeen_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = seventeen_coordinator 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 index ae31e1962d7..f54e7e94ac2 100644 --- a/homeassistant/components/seventeentrack/config_flow.py +++ b/homeassistant/components/seventeentrack/config_flow.py @@ -9,8 +9,7 @@ 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.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -47,7 +46,7 @@ USER_SCHEMA = vol.Schema( ) -class SeventeenTrackConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SeventeenTrackConfigFlow(ConfigFlow, domain=DOMAIN): """17track config flow.""" VERSION = 1 @@ -55,7 +54,7 @@ class SeventeenTrackConfigFlow(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) diff --git a/homeassistant/components/seventeentrack/const.py b/homeassistant/components/seventeentrack/const.py index 6f8ae1b221c..39932d31935 100644 --- a/homeassistant/components/seventeentrack/const.py +++ b/homeassistant/components/seventeentrack/const.py @@ -1,6 +1,9 @@ """Constants for the 17track.net component.""" from datetime import timedelta +import logging + +LOGGER = logging.getLogger(__package__) ATTR_DESTINATION_COUNTRY = "destination_country" ATTR_INFO_TEXT = "info_text" @@ -37,3 +40,8 @@ NOTIFICATION_DELIVERED_MESSAGE = ( ) VALUE_DELIVERED = "Delivered" + +SERVICE_GET_PACKAGES = "get_packages" + +ATTR_PACKAGE_STATE = "package_state" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" diff --git a/homeassistant/components/seventeentrack/coordinator.py b/homeassistant/components/seventeentrack/coordinator.py new file mode 100644 index 00000000000..4da4969ed92 --- /dev/null +++ b/homeassistant/components/seventeentrack/coordinator.py @@ -0,0 +1,84 @@ +"""Coordinator for 17Track.""" + +from dataclasses import dataclass +from typing import Any + +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError +from py17track.package import Package + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import slugify + +from .const import ( + CONF_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) + + +@dataclass +class SeventeenTrackData: + """Class for handling the data retrieval.""" + + summary: dict[str, dict[str, Any]] + live_packages: dict[str, Package] + + +class SeventeenTrackCoordinator(DataUpdateCoordinator[SeventeenTrackData]): + """Class to manage fetching 17Track data.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, client: SeventeenTrackClient) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + self.show_delivered = self.config_entry.options[CONF_SHOW_DELIVERED] + self.account_id = client.profile.account_id + + self.show_archived = self.config_entry.options[CONF_SHOW_ARCHIVED] + self.client = client + + async def _async_update_data(self) -> SeventeenTrackData: + """Fetch data from 17Track API.""" + + try: + summary = await self.client.profile.summary( + show_archived=self.show_archived + ) + + live_packages = set( + await self.client.profile.packages(show_archived=self.show_archived) + ) + + except SeventeenTrackError as err: + raise UpdateFailed(err) from err + + summary_dict = {} + live_packages_dict = {} + + for status, quantity in summary.items(): + summary_dict[slugify(status)] = { + "quantity": quantity, + "packages": [], + "status_name": status, + } + + for package in live_packages: + live_packages_dict[package.tracking_number] = package + summary_value = summary_dict.get(slugify(package.status)) + if summary_value: + summary_value["packages"].append(package) + + return SeventeenTrackData( + summary=summary_dict, live_packages=live_packages_dict + ) diff --git a/homeassistant/components/seventeentrack/icons.json b/homeassistant/components/seventeentrack/icons.json new file mode 100644 index 00000000000..78ca65edc4d --- /dev/null +++ b/homeassistant/components/seventeentrack/icons.json @@ -0,0 +1,33 @@ +{ + "entity": { + "sensor": { + "not_found": { + "default": "mdi:package" + }, + "in_transit": { + "default": "mdi:package" + }, + "expired": { + "default": "mdi:package" + }, + "ready_to_be_picked_up": { + "default": "mdi:package" + }, + "undelivered": { + "default": "mdi:package" + }, + "delivered": { + "default": "mdi:package" + }, + "returned": { + "default": "mdi:package" + }, + "package": { + "default": "mdi:package" + } + } + }, + "services": { + "get_packages": "mdi:package" + } +} diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 1de627fab39..acc8471c030 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -2,10 +2,8 @@ from __future__ import annotations -import logging +from typing import Any -from py17track.errors import SeventeenTrackError -from py17track.package import Package import voluptuous as vol from homeassistant.components import persistent_notification @@ -17,15 +15,16 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import config_validation as cv, entity, entity_registry as er +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.util import Throttle, slugify +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import SeventeenTrackCoordinator from .const import ( ATTR_DESTINATION_COUNTRY, ATTR_INFO_TEXT, @@ -39,17 +38,14 @@ from .const import ( ATTRIBUTION, CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, - DEFAULT_SCAN_INTERVAL, DOMAIN, - ENTITY_ID_TEMPLATE, + LOGGER, NOTIFICATION_DELIVERED_MESSAGE, NOTIFICATION_DELIVERED_TITLE, UNIQUE_ID_TEMPLATE, VALUE_DELIVERED, ) -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -111,81 +107,158 @@ async def async_setup_entry( ) -> None: """Set up a 17Track sensor entry.""" - client = hass.data[DOMAIN][config_entry.entry_id] + coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][config_entry.entry_id] + previous_tracking_numbers: set[str] = set() - data = SeventeenTrackData( - client, - async_add_entities, - DEFAULT_SCAN_INTERVAL, - config_entry.options[CONF_SHOW_ARCHIVED], - config_entry.options[CONF_SHOW_DELIVERED], - str(hass.config.time_zone), + @callback + def _async_create_remove_entities(): + live_tracking_numbers = set(coordinator.data.live_packages.keys()) + + new_tracking_numbers = live_tracking_numbers - previous_tracking_numbers + old_tracking_numbers = previous_tracking_numbers - live_tracking_numbers + + previous_tracking_numbers.update(live_tracking_numbers) + + packages_to_add = [ + coordinator.data.live_packages[tracking_number] + for tracking_number in new_tracking_numbers + ] + + for package_data in coordinator.data.live_packages.values(): + if ( + package_data.status == VALUE_DELIVERED + and not coordinator.show_delivered + ): + old_tracking_numbers.add(package_data.tracking_number) + notify_delivered( + hass, + package_data.friendly_name, + package_data.tracking_number, + ) + + remove_packages(hass, coordinator.account_id, old_tracking_numbers) + + async_add_entities( + SeventeenTrackPackageSensor( + coordinator, + package_data.tracking_number, + ) + for package_data in packages_to_add + if not ( + not coordinator.show_delivered and package_data.status == "Delivered" + ) + ) + + async_add_entities( + SeventeenTrackSummarySensor(status, coordinator) + for status, summary_data in coordinator.data.summary.items() + ) + + _async_create_remove_entities() + + config_entry.async_on_unload( + coordinator.async_add_listener(_async_create_remove_entities) ) - await data.async_update() -class SeventeenTrackSummarySensor(SensorEntity): - """Define a summary sensor.""" +class SeventeenTrackSensor(CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity): + """Define a 17Track sensor.""" _attr_attribution = ATTRIBUTION - _attr_icon = "mdi:package" + _attr_has_entity_name = True + + def __init__(self, coordinator: SeventeenTrackCoordinator) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.account_id)}, + entry_type=DeviceEntryType.SERVICE, + name="17Track", + ) + + +class SeventeenTrackSummarySensor(SeventeenTrackSensor): + """Define a summary sensor.""" + _attr_native_unit_of_measurement = "packages" - def __init__(self, data, status, initial_state) -> None: - """Initialize.""" - self._attr_extra_state_attributes = {} - self._data = data - self._state = initial_state + def __init__( + self, + status: str, + coordinator: SeventeenTrackCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) self._status = status - self._attr_name = f"Seventeentrack Packages {status}" - self._attr_unique_id = f"summary_{data.account_id}_{slugify(status)}" + self._attr_translation_key = status + self._attr_unique_id = f"summary_{coordinator.account_id}_{status}" @property def available(self) -> bool: """Return whether the entity is available.""" - return self._state is not None + return self._status in self.coordinator.data.summary + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.coordinator.data.summary[self._status]["quantity"] + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + packages = self.coordinator.data.summary[self._status]["packages"] + return { + ATTR_PACKAGES: [ + { + ATTR_TRACKING_NUMBER: package.tracking_number, + ATTR_LOCATION: package.location, + ATTR_STATUS: package.status, + ATTR_TIMESTAMP: package.timestamp, + ATTR_INFO_TEXT: package.info_text, + ATTR_FRIENDLY_NAME: package.friendly_name, + } + for package in packages + ] + } + + +class SeventeenTrackPackageSensor(SeventeenTrackSensor): + """Define an individual package sensor.""" + + _attr_translation_key = "package" + + def __init__( + self, + coordinator: SeventeenTrackCoordinator, + tracking_number: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._tracking_number = tracking_number + self._previous_status = coordinator.data.live_packages[tracking_number].status + self._attr_unique_id = UNIQUE_ID_TEMPLATE.format( + coordinator.account_id, tracking_number + ) + package = coordinator.data.live_packages[tracking_number] + if not (name := package.friendly_name): + name = tracking_number + self._attr_translation_placeholders = {"name": name} + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + return self._tracking_number in self.coordinator.data.live_packages @property def native_value(self) -> StateType: """Return the state.""" - return self._state + return self.coordinator.data.live_packages[self._tracking_number].status - async def async_update(self) -> None: - """Update the sensor.""" - await self._data.async_update() - - package_data = [] - for package in self._data.packages.values(): - if package.status != self._status: - continue - - package_data.append( - { - ATTR_FRIENDLY_NAME: package.friendly_name, - ATTR_INFO_TEXT: package.info_text, - ATTR_TIMESTAMP: package.timestamp, - ATTR_STATUS: package.status, - ATTR_LOCATION: package.location, - ATTR_TRACKING_NUMBER: package.tracking_number, - } - ) - - self._attr_extra_state_attributes[ATTR_PACKAGES] = ( - package_data if package_data else None - ) - - self._state = self._data.summary.get(self._status) - - -class SeventeenTrackPackageSensor(SensorEntity): - """Define an individual package sensor.""" - - _attr_attribution = ATTRIBUTION - _attr_icon = "mdi:package" - - def __init__(self, data, package) -> None: - """Initialize.""" - self._attr_extra_state_attributes = { + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + package = self.coordinator.data.live_packages[self._tracking_number] + return { ATTR_DESTINATION_COUNTRY: package.destination_country, ATTR_INFO_TEXT: package.info_text, ATTR_TIMESTAMP: package.timestamp, @@ -195,158 +268,30 @@ class SeventeenTrackPackageSensor(SensorEntity): ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language, ATTR_TRACKING_NUMBER: package.tracking_number, } - self._data = data - self._friendly_name = package.friendly_name - self._state = package.status - self._tracking_number = package.tracking_number - self.entity_id = ENTITY_ID_TEMPLATE.format(self._tracking_number) - self._attr_unique_id = UNIQUE_ID_TEMPLATE.format( - data.account_id, self._tracking_number - ) - @property - def available(self) -> bool: - """Return whether the entity is available.""" - return self._data.packages.get(self._tracking_number) is not None - @property - 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) -> StateType: - """Return the state.""" - return self._state - - async def async_update(self) -> None: - """Update the sensor.""" - await self._data.async_update() - - if not self.available: - # Entity cannot be removed while its being added - async_call_later(self.hass, 1, self._remove) - return - - package = self._data.packages.get(self._tracking_number, None) - - # If the user has elected to not see delivered packages and one gets - # delivered, post a notification: - if package.status == VALUE_DELIVERED and not self._data.show_delivered: - self._notify_delivered() - # Entity cannot be removed while its being added - async_call_later(self.hass, 1, self._remove) - return - - self._attr_extra_state_attributes.update( - { - ATTR_INFO_TEXT: package.info_text, - ATTR_TIMESTAMP: package.timestamp, - ATTR_LOCATION: package.location, - } - ) - self._state = package.status - self._friendly_name = package.friendly_name - - async def _remove(self, *_): - """Remove entity itself.""" - await self.async_remove(force_remove=True) - - reg = er.async_get(self.hass) +def remove_packages(hass: HomeAssistant, account_id: str, packages: set[str]) -> None: + """Remove entity itself.""" + reg = er.async_get(hass) + for package in packages: entity_id = reg.async_get_entity_id( "sensor", "seventeentrack", - UNIQUE_ID_TEMPLATE.format(self._data.account_id, self._tracking_number), + UNIQUE_ID_TEMPLATE.format(account_id, package), ) if entity_id: reg.async_remove(entity_id) - def _notify_delivered(self): - """Notify when package is delivered.""" - _LOGGER.info("Package delivered: %s", self._tracking_number) - identification = ( - self._friendly_name if self._friendly_name else self._tracking_number - ) - message = NOTIFICATION_DELIVERED_MESSAGE.format( - identification, self._tracking_number - ) - title = NOTIFICATION_DELIVERED_TITLE.format(identification) - notification_id = NOTIFICATION_DELIVERED_TITLE.format(self._tracking_number) +def notify_delivered(hass: HomeAssistant, friendly_name: str, tracking_number: str): + """Notify when package is delivered.""" + LOGGER.debug("Package delivered: %s", tracking_number) - persistent_notification.create( - self.hass, message, title=title, notification_id=notification_id - ) + identification = friendly_name if friendly_name else tracking_number + message = NOTIFICATION_DELIVERED_MESSAGE.format(identification, tracking_number) + title = NOTIFICATION_DELIVERED_TITLE.format(identification) + notification_id = NOTIFICATION_DELIVERED_TITLE.format(tracking_number) - -class SeventeenTrackData: - """Define a data handler for 17track.net.""" - - def __init__( - self, - client, - async_add_entities, - scan_interval, - 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: dict[str, Package] = {} - self.show_delivered = show_delivered - self.timezone = timezone - self.summary: dict[str, int] = {} - self.async_update = Throttle(self._scan_interval)(self._async_update) - self.first_update = True - - async def _async_update(self): - """Get updated data from 17track.net.""" - entities: list[entity.Entity] = [] - - try: - packages = await self._client.profile.packages( - show_archived=self._show_archived, tz=self.timezone - ) - _LOGGER.debug("New package data received: %s", packages) - - new_packages = {p.tracking_number: p for p in packages} - - to_add = set(new_packages) - set(self.packages) - - _LOGGER.debug("Will add new tracking numbers: %s", to_add) - if to_add: - entities.extend( - SeventeenTrackPackageSensor(self, new_packages[tracking_number]) - for tracking_number in to_add - ) - - self.packages = new_packages - except SeventeenTrackError as err: - _LOGGER.error("There was an error retrieving packages: %s", err) - - try: - self.summary = await self._client.profile.summary( - show_archived=self._show_archived - ) - _LOGGER.debug("New summary data received: %s", self.summary) - - # creating summary sensors on first update - if self.first_update: - self.first_update = False - entities.extend( - SeventeenTrackSummarySensor(self, status, quantity) - for status, quantity in self.summary.items() - ) - - except SeventeenTrackError as err: - _LOGGER.error("There was an error retrieving the summary: %s", err) - self.summary = {} - - self._async_add_entities(entities, True) + persistent_notification.create( + hass, message, title=title, notification_id=notification_id + ) diff --git a/homeassistant/components/seventeentrack/services.yaml b/homeassistant/components/seventeentrack/services.yaml new file mode 100644 index 00000000000..41cb66ada5f --- /dev/null +++ b/homeassistant/components/seventeentrack/services.yaml @@ -0,0 +1,20 @@ +get_packages: + fields: + package_state: + selector: + select: + multiple: true + options: + - "not_found" + - "in_transit" + - "expired" + - "ready_to_be_picked_up" + - "undelivered" + - "delivered" + - "returned" + translation_key: package_state + config_entry_id: + required: true + selector: + config_entry: + integration: seventeentrack diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index 39ddb5ef8ef..626af29e856 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -38,5 +38,62 @@ "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." } + }, + "entity": { + "sensor": { + "not_found": { + "name": "Not found" + }, + "in_transit": { + "name": "In transit" + }, + "expired": { + "name": "Expired" + }, + "ready_to_be_picked_up": { + "name": "Ready to be picked up" + }, + "undelivered": { + "name": "Undelivered" + }, + "delivered": { + "name": "Delivered" + }, + "returned": { + "name": "Returned" + }, + "package": { + "name": "Package {name}" + } + } + }, + "services": { + "get_packages": { + "name": "Get packages", + "description": "Get packages from 17Track", + "fields": { + "package_state": { + "name": "Package states", + "description": "Only return packages with the specified states. Returns all packages if not specified." + }, + "config_entry_id": { + "name": "17Track service", + "description": "The packages will be retrieved for the selected service." + } + } + } + }, + "selector": { + "package_state": { + "options": { + "not_found": "[%key:component::seventeentrack::entity::sensor::not_found::name%]", + "in_transit": "[%key:component::seventeentrack::entity::sensor::in_transit::name%]", + "expired": "[%key:component::seventeentrack::entity::sensor::expired::name%]", + "ready_to_be_picked_up": "[%key:component::seventeentrack::entity::sensor::ready_to_be_picked_up::name%]", + "undelivered": "[%key:component::seventeentrack::entity::sensor::undelivered::name%]", + "delivered": "[%key:component::seventeentrack::entity::sensor::delivered::name%]", + "returned": "[%key:component::seventeentrack::entity::sensor::returned::name%]" + } + } } } diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py index 8d5d4708e0e..f328e6453cc 100644 --- a/homeassistant/components/sharkiq/const.py +++ b/homeassistant/components/sharkiq/const.py @@ -12,6 +12,7 @@ PLATFORMS = [Platform.VACUUM] DOMAIN = "sharkiq" SHARK = "Shark" UPDATE_INTERVAL = timedelta(seconds=30) +SERVICE_CLEAN_ROOM = "clean_room" SHARKIQ_REGION_EUROPE = "europe" SHARKIQ_REGION_ELSEWHERE = "elsewhere" diff --git a/homeassistant/components/sharkiq/icons.json b/homeassistant/components/sharkiq/icons.json new file mode 100644 index 00000000000..13fd58ce66d --- /dev/null +++ b/homeassistant/components/sharkiq/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "clean_room": "mdi:robot-vacuum" + } +} diff --git a/homeassistant/components/sharkiq/services.yaml b/homeassistant/components/sharkiq/services.yaml new file mode 100644 index 00000000000..7f82ed40702 --- /dev/null +++ b/homeassistant/components/sharkiq/services.yaml @@ -0,0 +1,15 @@ +clean_room: + target: + entity: + integration: "sharkiq" + domain: "vacuum" + + fields: + rooms: + required: true + advanced: false + example: "Kitchen" + default: "" + selector: + area: + multiple: true diff --git a/homeassistant/components/sharkiq/strings.json b/homeassistant/components/sharkiq/strings.json index 23f949be4cc..c1648332975 100644 --- a/homeassistant/components/sharkiq/strings.json +++ b/homeassistant/components/sharkiq/strings.json @@ -40,5 +40,22 @@ "elsewhere": "Everywhere Else" } } + }, + "exceptions": { + "invalid_room": { + "message": "The room { room } is unavailable to your vacuum. Make sure all rooms match the Shark App, including capitalization." + } + }, + "services": { + "clean_room": { + "name": "Clean Room", + "description": "Cleans a specific user-defined room or set of rooms.", + "fields": { + "rooms": { + "name": "Rooms", + "description": "List of rooms to clean" + } + } + } } } diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 658d446b9cb..d028b0b8b87 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -6,6 +6,7 @@ from collections.abc import Iterable from typing import Any from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum +import voluptuous as vol from homeassistant.components.vacuum import ( STATE_CLEANING, @@ -18,11 +19,14 @@ from homeassistant.components.vacuum import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv 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, SHARK +from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK from .update_coordinator import SharkIqUpdateCoordinator OPERATING_STATE_MAP = { @@ -45,7 +49,7 @@ ATTR_ERROR_CODE = "last_error_code" ATTR_ERROR_MSG = "last_error_message" ATTR_LOW_LIGHT = "low_light" ATTR_RECHARGE_RESUME = "recharge_and_resume" -ATTR_RSSI = "rssi" +ATTR_ROOMS = "rooms" async def async_setup_entry( @@ -64,6 +68,17 @@ async def async_setup_entry( ) async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_CLEAN_ROOM, + { + vol.Required(ATTR_ROOMS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + }, + "async_clean_room", + ) + class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuumEntity): """Shark IQ vacuum entity.""" @@ -81,6 +96,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum | VacuumEntityFeature.STOP | VacuumEntityFeature.LOCATE ) + _unrecorded_attributes = frozenset({ATTR_ROOMS}) def __init__( self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator @@ -136,7 +152,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum @property def operating_mode(self) -> str | None: - """Operating mode..""" + """Operating mode.""" op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) return OPERATING_STATE_MAP.get(op_mode) @@ -192,6 +208,24 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum """Cause the device to generate a loud chirp.""" await self.sharkiq.async_find_device() + async def async_clean_room(self, rooms: list[str], **kwargs: Any) -> None: + """Clean specific rooms.""" + rooms_to_clean = [] + valid_rooms = self.available_rooms or [] + for room in rooms: + if room in valid_rooms: + rooms_to_clean.append(room) + else: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_room", + translation_placeholders={"room": room}, + ) + + LOGGER.debug("Cleaning room(s): %s", rooms_to_clean) + await self.sharkiq.async_clean_rooms(rooms_to_clean) + await self.coordinator.async_refresh() + @property def fan_speed(self) -> str | None: """Return the current fan speed.""" @@ -225,13 +259,18 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum """Let us know if the robot is operating in low-light mode.""" return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION) + @property + def available_rooms(self) -> list | None: + """Return a list of rooms available to clean.""" + return self.sharkiq.get_room_list() + @property def extra_state_attributes(self) -> dict[str, Any]: """Return a dictionary of device state attributes specific to sharkiq.""" - data = { + return { ATTR_ERROR_CODE: self.error_code, ATTR_ERROR_MSG: self.sharkiq.error_text, ATTR_LOW_LIGHT: self.low_light, ATTR_RECHARGE_RESUME: self.recharge_resume, + ATTR_ROOMS: self.available_rooms, } - return data diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 91cb48e9988..c2c384e39aa 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -15,7 +15,7 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import JsonObjectType @@ -58,8 +58,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: rendered_args = args_compiled.async_render( variables=service.data, parse_result=False ) - except TemplateError as ex: - _LOGGER.exception("Error rendering command template: %s", ex) + except TemplateError: + _LOGGER.exception("Error rendering command template") raise else: rendered_args = None @@ -91,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: async with asyncio.timeout(COMMAND_TIMEOUT): stdout_data, stderr_data = await process.communicate() - except TimeoutError: + except TimeoutError as err: _LOGGER.error( "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT ) @@ -103,7 +103,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: process._transport.close() # type: ignore[attr-defined] del process - raise + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="timeout", + translation_placeholders={ + "command": cmd, + "timeout": str(COMMAND_TIMEOUT), + }, + ) from err if stdout_data: _LOGGER.debug( @@ -135,12 +142,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: service_response["stdout"] = stdout_data.decode("utf-8").strip() if stderr_data: service_response["stderr"] = stderr_data.decode("utf-8").strip() - return service_response - except UnicodeDecodeError: + except UnicodeDecodeError as err: _LOGGER.exception( "Unable to handle non-utf8 output of command: `%s`", cmd ) - raise + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="non_utf8_output", + translation_placeholders={"command": cmd}, + ) from err + return service_response return None for name in conf: diff --git a/homeassistant/components/shell_command/strings.json b/homeassistant/components/shell_command/strings.json new file mode 100644 index 00000000000..c87dac15b2d --- /dev/null +++ b/homeassistant/components/shell_command/strings.json @@ -0,0 +1,10 @@ +{ + "exceptions": { + "timeout": { + "message": "Timed out running command: `{command}`, after: {timeout} seconds" + }, + "non_utf8_output": { + "message": "Unable to handle non-utf8 output of command: `{command}`" + } + } +} diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 7d23a1cd57d..cfeab531687 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -3,23 +3,22 @@ from __future__ import annotations import contextlib -from typing import Any, Final +from typing import Final -from aioshelly.block_device import BlockDevice, BlockUpdateType +from aioshelly.block_device import BlockDevice from aioshelly.common import ConnectionOptions from aioshelly.const import DEFAULT_COAP_PORT, RPC_GENERATIONS from aioshelly.exceptions import ( DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, MacAddressMismatchError, ) -from aioshelly.rpc_device import RpcDevice, RpcUpdateType +from aioshelly.rpc_device import RpcDevice import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,12 +52,9 @@ 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, ) @@ -154,7 +150,6 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b async_get_clientsession(hass), coap_context, options, - False, ) dev_reg = dr_async_get(hass) @@ -186,57 +181,38 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b data[CONF_SLEEP_PERIOD] = sleep_period = BLOCK_EXPECTED_SLEEP_PERIOD hass.config_entries.async_update_entry(entry, data=data) - async def _async_block_device_setup() -> None: - """Set up a block based device that is online.""" - shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) - shelly_entry_data.block.async_setup() - - platforms = BLOCK_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) - platforms = BLOCK_PLATFORMS - - await hass.config_entries.async_forward_entry_setups(entry, platforms) - - @callback - def _async_device_online(_: Any, update_type: BlockUpdateType) -> None: - LOGGER.debug("Device %s is online, resuming setup", entry.title) - shelly_entry_data.device = None - - if sleep_period is None: - data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_block_device_sleep_period(device.settings) - data["model"] = device.settings["device"]["type"] - hass.config_entries.async_update_entry(entry, data=data) - - hass.async_create_task(_async_block_device_setup(), eager_start=True) - if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online block device %s", entry.title) try: await device.initialize() + if not device.firmware_supported: + async_create_issue_unsupported_firmware(hass, entry) + raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - except FirmwareUnsupported as err: - async_create_issue_unsupported_firmware(hass, entry) - raise ConfigEntryNotReady from err - await _async_block_device_setup() + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup() + shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) + await hass.config_entries.async_forward_entry_setups(entry, BLOCK_PLATFORMS) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - device.subscribe_updates(_async_device_online) + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup(BLOCK_SLEEPING_PLATFORMS) else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - await _async_block_device_setup() + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup() + await hass.config_entries.async_forward_entry_setups( + entry, BLOCK_SLEEPING_PLATFORMS + ) ir.async_delete_issue( hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) @@ -260,7 +236,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo async_get_clientsession(hass), ws_context, options, - False, ) dev_reg = dr_async_get(hass) @@ -276,58 +251,38 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - async def _async_rpc_device_setup() -> None: - """Set up a RPC based device that is online.""" - shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) - shelly_entry_data.rpc.async_setup() - - platforms = RPC_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator( - hass, entry, device - ) - platforms = RPC_PLATFORMS - - await hass.config_entries.async_forward_entry_setups(entry, platforms) - - @callback - def _async_device_online(_: Any, update_type: RpcUpdateType) -> None: - LOGGER.debug("Device %s is online, resuming setup", entry.title) - shelly_entry_data.device = None - - if sleep_period is None: - data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_rpc_device_wakeup_period(device.status) - hass.config_entries.async_update_entry(entry, data=data) - - hass.async_create_task(_async_rpc_device_setup(), eager_start=True) - if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online RPC device %s", entry.title) try: await device.initialize() - except FirmwareUnsupported as err: - async_create_issue_unsupported_firmware(hass, entry) - raise ConfigEntryNotReady from err + if not device.firmware_supported: + async_create_issue_unsupported_firmware(hass, entry) + raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - await _async_rpc_device_setup() + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup() + shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device) + await hass.config_entries.async_forward_entry_setups(entry, RPC_PLATFORMS) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - device.subscribe_updates(_async_device_online) + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup(RPC_SLEEPING_PLATFORMS) else: # Restore sensors for sleeping device - LOGGER.debug("Setting up offline block device %s", entry.title) - await _async_rpc_device_setup() + LOGGER.debug("Setting up offline RPC device %s", entry.title) + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup() + await hass.config_entries.async_forward_entry_setups( + entry, RPC_SLEEPING_PLATFORMS + ) ir.async_delete_issue( hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) @@ -339,11 +294,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" shelly_entry_data = get_entry_data(hass)[entry.entry_id] - # If device is present, block/rpc coordinator is not setup yet - if (device := shelly_entry_data.device) is not None: - await async_shutdown_device(device) - return True - platforms = RPC_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): platforms = RPC_PLATFORMS diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index b368b38820e..81289bc1a9b 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -132,7 +132,11 @@ def async_setup_rpc_entry( climate_ids = [] for id_ in climate_key_ids: climate_ids.append(id_) - + # There are three configuration scenarios for WallDisplay: + # - relay mode (no thermostat) + # - thermostat mode using the internal relay as an actuator + # - thermostat mode using an external (from another device) relay as + # an actuator if is_rpc_thermostat_internal_actuator(coordinator.device.status): # Wall Display relay is used as the thermostat actuator, # we need to remove a switch entity diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 24b66e15893..46cea4e49a4 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -11,7 +11,6 @@ from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATION from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, ) from aioshelly.rpc_device import RpcDevice @@ -103,6 +102,7 @@ async def validate_input( ws_context, options, ) + await rpc_device.initialize() await rpc_device.shutdown() sleep_period = get_rpc_device_wakeup_period(rpc_device.status) @@ -121,6 +121,7 @@ async def validate_input( coap_context, options, ) + await block_device.initialize() block_device.shutdown() return { "title": block_device.name, @@ -154,8 +155,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, port) except DeviceConnectionError: errors["base"] = "cannot_connect" - except FirmwareUnsupported: - return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -287,8 +286,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, DEFAULT_HTTP_PORT) except DeviceConnectionError: return self.async_abort(reason="cannot_connect") - except FirmwareUnsupported: - return self.async_abort(reason="unsupported_firmware") if not mac: # We could not get the mac address from the name @@ -366,14 +363,14 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await self._async_get_info(host, port) - except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): + except (DeviceConnectionError, InvalidAuthError): 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, port, info, user_input) - except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): + except (DeviceConnectionError, InvalidAuthError): return self.async_abort(reason="reauth_unsuccessful") return self.async_update_reload_and_abort( diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index c52585c3363..e321f393ba3 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -15,7 +15,12 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from aioshelly.rpc_device import RpcDevice, RpcUpdateType from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.debounce import Debouncer @@ -58,7 +63,9 @@ from .const import ( BLEScannerMode, ) from .utils import ( + async_create_issue_unsupported_firmware, async_shutdown_device, + get_block_device_sleep_period, get_device_entry_gen, get_http_port, get_rpc_device_wakeup_period, @@ -73,7 +80,6 @@ class ShellyEntryData: """Class for sharing data within a given config entry.""" block: ShellyBlockCoordinator | None = None - device: BlockDevice | RpcDevice | None = None rest: ShellyRestCoordinator | None = None rpc: ShellyRpcCoordinator | None = None rpc_poll: ShellyRpcPollingCoordinator | None = None @@ -98,6 +104,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): self.entry = entry self.device = device self.device_id: str | None = None + self._pending_platforms: list[Platform] | None = None device_name = device.name if device.initialized else entry.title interval_td = timedelta(seconds=update_interval) super().__init__(hass, LOGGER, name=device_name, update_interval=interval_td) @@ -131,8 +138,9 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): """Sleep period of the device.""" return self.entry.data.get(CONF_SLEEP_PERIOD, 0) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" + self._pending_platforms = pending_platforms dev_reg = dr_async_get(self.hass) device_entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, @@ -146,6 +154,50 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): ) self.device_id = device_entry.id + async def _async_device_connect_task(self) -> bool: + """Connect to a Shelly device task.""" + LOGGER.debug("Connecting to Shelly Device - %s", self.name) + try: + await self.device.initialize() + update_device_fw_info(self.hass, self.device, self.entry) + except DeviceConnectionError as err: + LOGGER.debug( + "Error connecting to Shelly device %s, error: %r", self.name, err + ) + return False + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) + return False + + if not self.device.firmware_supported: + async_create_issue_unsupported_firmware(self.hass, self.entry) + return False + + if not self._pending_platforms: + return True + + LOGGER.debug("Device %s is online, resuming setup", self.entry.title) + platforms = self._pending_platforms + self._pending_platforms = None + + data = {**self.entry.data} + + # Update sleep_period + old_sleep_period = data[CONF_SLEEP_PERIOD] + if isinstance(self.device, RpcDevice): + new_sleep_period = get_rpc_device_wakeup_period(self.device.status) + elif isinstance(self.device, BlockDevice): + new_sleep_period = get_block_device_sleep_period(self.device.settings) + + if new_sleep_period != old_sleep_period: + data[CONF_SLEEP_PERIOD] = new_sleep_period + self.hass.config_entries.async_update_entry(self.entry, data=data) + + # Resume platform setup + await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + + return True + async def _async_reload_entry(self) -> None: """Reload entry.""" self._debounced_reload.async_cancel() @@ -179,7 +231,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self._last_cfg_changed: int | None = None self._last_mode: str | None = None - self._last_effect: int | None = None + self._last_effect: str | None = None self._last_input_events_count: dict = {} self._last_target_temp: float | None = None self._push_update_failures: int = 0 @@ -189,9 +241,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) @callback @@ -213,15 +263,14 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if not self.device.initialized: return - assert self.device.blocks - # For buttons which are battery powered - set initial value for last_event_count if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: for block in self.device.blocks: if block.type != "device": continue - if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": + wakeup_event = cast(list, block.wakeupEvent) + if len(wakeup_event) == 1 and wakeup_event[0] == "button": self._last_input_events_count[1] = -1 break @@ -230,7 +279,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): cfg_changed = 0 for block in self.device.blocks: if block.type == "device" and block.cfgChanged is not None: - cfg_changed = block.cfgChanged + cfg_changed = cast(int, block.cfgChanged) # Shelly TRV sends information about changing the configuration for no # reason, reloading the config entry is not needed for it. @@ -316,14 +365,21 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self, device_: BlockDevice, update_type: BlockUpdateType ) -> None: """Handle device update.""" - if update_type == BlockUpdateType.COAP_PERIODIC: + if update_type is BlockUpdateType.ONLINE: + self.entry.async_create_background_task( + self.hass, + self._async_device_connect_task(), + "block device online", + eager_start=True, + ) + elif update_type is BlockUpdateType.COAP_PERIODIC: self._push_update_failures = 0 ir.async_delete_issue( self.hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), ) - elif update_type == BlockUpdateType.COAP_REPLY: + elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: LOGGER.debug( @@ -348,9 +404,9 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): ) self.async_set_updated_data(None) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" - super().async_setup() + super().async_setup(pending_platforms) self.device.subscribe_updates(self._async_handle_update) def shutdown(self) -> None: @@ -420,9 +476,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) entry.async_on_unload(entry.add_update_listener(self._async_update_listener)) @@ -542,14 +596,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: return - LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) - try: - await self.device.initialize() - update_device_fw_info(self.hass, self.device, self.entry) - except DeviceConnectionError as err: - raise UpdateFailed(f"Device disconnected: {repr(err)}") from err - except InvalidAuthError: - await self.async_shutdown_device_and_start_reauth() + if not await self._async_device_connect_task(): + raise UpdateFailed("Device reconnect error") async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -616,11 +664,25 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self, device_: RpcDevice, update_type: RpcUpdateType ) -> None: """Handle device update.""" - if update_type is RpcUpdateType.INITIALIZED: - self.hass.async_create_task(self._async_connected(), eager_start=True) + if update_type is RpcUpdateType.ONLINE: + self.entry.async_create_background_task( + self.hass, + self._async_device_connect_task(), + "rpc device online", + eager_start=True, + ) + elif update_type is RpcUpdateType.INITIALIZED: + self.entry.async_create_background_task( + self.hass, self._async_connected(), "rpc device init", eager_start=True + ) self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: - self.hass.async_create_task(self._async_disconnected(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_disconnected(), + "rpc device disconnected", + eager_start=True, + ) elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: @@ -628,13 +690,15 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): elif update_type is RpcUpdateType.EVENT and (event := self.device.event): self._async_device_event_handler(event) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" - super().async_setup() + super().async_setup(pending_platforms) self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected - self.hass.async_create_task(self._async_connected(), eager_start=True) + self.entry.async_create_task( + self.hass, self._async_connected(), eager_start=True + ) async def shutdown(self) -> None: """Shutdown the coordinator.""" @@ -717,4 +781,9 @@ async def async_reconnect_soon(hass: HomeAssistant, entry: ConfigEntry) -> None: and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) ): - hass.async_create_task(coordinator.async_request_refresh(), eager_start=True) + entry.async_create_background_task( + hass, + coordinator.async_request_refresh(), + "reconnect soon", + eager_start=True, + ) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index accca5f1a64..150244e2e47 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -74,7 +74,7 @@ def async_setup_block_attribute_entities( for block in coordinator.device.blocks: for sensor_id in block.sensor_ids: - description = sensors.get((block.type, sensor_id)) + description = sensors.get((cast(str, block.type), sensor_id)) if description is None: continue diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index d0590fc7c20..0650e2d15e5 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -78,7 +78,7 @@ def async_setup_block_entry( for block in coordinator.device.blocks: if block.type == "light": blocks.append(block) - elif block.type == "relay": + elif block.type == "relay" and block.channel is not None: if not is_block_channel_type_light( coordinator.device.settings, int(block.channel) ): diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 06159cb543b..08971713ced 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==8.2.0"], + "requirements": ["aioshelly==9.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index d4a8b117f4c..cee27e9ca07 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -38,7 +38,6 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "unsupported_firmware": "The device is using an unsupported firmware version.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." } diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 48ff337d22a..81b16d48ab8 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -43,6 +43,7 @@ from .utils import ( is_block_channel_type_light, is_rpc_channel_type_light, is_rpc_thermostat_internal_actuator, + is_rpc_thermostat_mode, ) @@ -104,8 +105,12 @@ def async_setup_block_entry( relay_blocks = [] assert coordinator.device.blocks for block in coordinator.device.blocks: - if block.type != "relay" or is_block_channel_type_light( - coordinator.device.settings, int(block.channel) + if ( + block.type != "relay" + or block.channel is not None + and is_block_channel_type_light( + coordinator.device.settings, int(block.channel) + ) ): continue @@ -136,12 +141,19 @@ def async_setup_rpc_entry( continue if coordinator.model == MODEL_WALL_DISPLAY: - if not is_rpc_thermostat_internal_actuator(coordinator.device.status): - # Wall Display relay is not used as the thermostat actuator, - # we need to remove a climate entity + # There are three configuration scenarios for WallDisplay: + # - relay mode (no thermostat) + # - thermostat mode using the internal relay as an actuator + # - thermostat mode using an external (from another device) relay as + # an actuator + if not is_rpc_thermostat_mode(id_, coordinator.device.status): + # The device is not in thermostat mode, we need to remove a climate + # entity unique_id = f"{coordinator.mac}-thermostat:{id_}" async_remove_shelly_entity(hass, "climate", unique_id) - else: + elif is_rpc_thermostat_internal_actuator(coordinator.device.status): + # The internal relay is an actuator, skip this ID so as not to create + # a switch entity continue switch_ids.append(id_) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 56ad1f2ef67..dc6e9c9698a 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -296,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): await self.coordinator.async_shutdown_device_and_start_reauth() else: self._ota_in_progress = True - LOGGER.info("OTA update call for %s successful", self.coordinator.name) + LOGGER.debug("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 ce98e0d5c12..b7cb2f1476a 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -500,3 +500,8 @@ def async_remove_shelly_rpc_entities( 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) + + +def is_rpc_thermostat_mode(ident: int, status: dict[str, Any]) -> bool: + """Return True if 'thermostat:' is present in the status.""" + return f"thermostat:{ident}" in status diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index f7a7a1f06e4..4329154b069 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -77,8 +77,8 @@ def validate_input(data: dict[str, Any]) -> dict[str, str] | None: return {"base": "invalid_account_format"} except InvalidAccountLengthError: return {"base": "invalid_account_length"} - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception from SIAAccount: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception from SIAAccount") return {"base": "unknown"} if not 1 <= data[CONF_PING_INTERVAL] <= 1440: return {"base": "invalid_ping"} diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 58cd85fb26e..9c8846b2767 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -94,7 +94,7 @@ class SignalNotificationService(BaseNotificationService): data = DATA_SCHEMA(data) except vol.Invalid as ex: _LOGGER.error("Invalid message data: %s", ex) - raise ex + raise filenames = self.get_filenames(data) attachments_as_bytes = self.get_attachments_as_bytes( @@ -107,7 +107,7 @@ class SignalNotificationService(BaseNotificationService): ) except SignalCliRestApiError as ex: _LOGGER.error("%s", ex) - raise ex + raise @staticmethod def get_filenames(data: Any) -> list[str] | None: @@ -174,7 +174,7 @@ class SignalNotificationService(BaseNotificationService): attachments_as_bytes.append(chunks) except Exception as ex: _LOGGER.error("%s", ex) - raise ex + raise if not attachments_as_bytes: return None diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index a083aa9d702..a0a599dd2df 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, TypedDict, cast, final +from typing import Any, TypedDict, cast, final import voluptuous as vol @@ -40,11 +40,6 @@ from .const import ( # noqa: F401 SirenEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 52c56993be0..bc4a0fdc743 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -54,11 +54,10 @@ class SkyHubDeviceScanner(DeviceScanner): async def async_get_device_name(self, device): """Return the name of the given device.""" - name = next( + return next( (result.name for result in self.last_results if result.mac == device), None, ) - return name async def async_get_extra_attributes(self, device): """Get extra attributes of a device.""" diff --git a/homeassistant/components/slack/config_flow.py b/homeassistant/components/slack/config_flow.py index b23dc60da60..03f3683e5a9 100644 --- a/homeassistant/components/slack/config_flow.py +++ b/homeassistant/components/slack/config_flow.py @@ -68,7 +68,7 @@ class SlackFlowHandler(ConfigFlow, domain=DOMAIN): if ex.response["error"] == "invalid_auth": return "invalid_auth", None return "cannot_connect", None - except Exception as ex: # pylint:disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint:disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown", None return None, info diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 06fc76e217a..a18b211962a 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -262,7 +262,7 @@ class SlackNotificationService(BaseNotificationService): } results = await asyncio.gather(*tasks.values(), return_exceptions=True) - for target, result in zip(tasks, results): + for target, result in zip(tasks, results, strict=False): if isinstance(result, SlackApiError): _LOGGER.error( "There was a Slack API error while sending to %s: %r", diff --git a/homeassistant/components/smart_meter_texas/manifest.json b/homeassistant/components/smart_meter_texas/manifest.json index 1b18bbb2bc9..8bf44fbed15 100644 --- a/homeassistant/components/smart_meter_texas/manifest.json +++ b/homeassistant/components/smart_meter_texas/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/smart_meter_texas", "iot_class": "cloud_polling", "loggers": ["smart_meter_texas"], - "requirements": ["smart-meter-texas==0.4.7"] + "requirements": ["smart-meter-texas==0.5.5"] } diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 8136806cd0b..9bfa11d3293 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -28,6 +28,7 @@ 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 homeassistant.setup import SetupPhases, async_pause_setup from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( @@ -170,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Setup device broker - broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PLATFORMS): + # DeviceBroker has a side effect of importing platform + # modules when its created. In the future this should be + # refactored to not do this. + broker = await hass.async_add_import_executor_job( + DeviceBroker, hass, entry, token, smart_app, devices, scenes + ) broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index b5fac0b34f6..532234f4059 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -96,14 +96,12 @@ class SmartTubLight(SmartTubEntity, LightEntity): @property def effect_list(self): """Return the list of supported effects.""" - effects = [ + return [ effect for effect in map(self._light_mode_to_effect, SpaLight.LightMode) if effect is not None ] - return effects - @staticmethod def _light_mode_to_effect(light_mode: SpaLight.LightMode): if light_mode == SpaLight.LightMode.OFF: diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 50abc9b39ef..1ed1f66570f 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -92,7 +92,6 @@ class Gateway: start = True entries = [] all_parts = -1 - all_parts_arrived = False _LOGGER.debug("Start remaining:%i", start_remaining) try: @@ -101,42 +100,38 @@ class Gateway: entry = state_machine.GetNextSMS(Folder=0, Start=True) all_parts = entry[0]["UDH"]["AllParts"] part_number = entry[0]["UDH"]["PartNumber"] - is_single_part = all_parts == 0 - is_multi_part = 0 <= all_parts < start_remaining + part_is_missing = all_parts > start_remaining _LOGGER.debug("All parts:%i", all_parts) _LOGGER.debug("Part Number:%i", part_number) _LOGGER.debug("Remaining:%i", remaining) - all_parts_arrived = is_multi_part or is_single_part - _LOGGER.debug("Start all_parts_arrived:%s", all_parts_arrived) + _LOGGER.debug("Start is_part_missing:%s", part_is_missing) start = False else: entry = state_machine.GetNextSMS( Folder=0, Location=entry[0]["Location"] ) - if all_parts_arrived or force: - remaining = remaining - 1 - entries.append(entry) - - # delete retrieved sms - _LOGGER.debug("Deleting message") - try: - state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) - except gammu.ERR_MEMORY_NOT_AVAILABLE: - _LOGGER.error("Error deleting SMS, memory not available") - else: + if part_is_missing and not force: _LOGGER.debug("Not all parts have arrived") break + remaining = remaining - 1 + entries.append(entry) + + # delete retrieved sms + _LOGGER.debug("Deleting message") + try: + state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) + except gammu.ERR_MEMORY_NOT_AVAILABLE: + _LOGGER.error("Error deleting SMS, memory not available") + except gammu.ERR_EMPTY: # error is raised if memory is empty (this induces wrong reported # memory status) _LOGGER.info("Failed to read messages!") # Link all SMS when there are concatenated messages - entries = gammu.LinkSMS(entries) - - return entries + return gammu.LinkSMS(entries) @callback def _notify_incoming_sms(self, messages): @@ -210,7 +205,7 @@ async def create_sms_gateway(config, hass): _LOGGER.error("Failed to initialize, error %s", exc) await gateway.terminate_async() return None - return gateway except gammu.GSMError as exc: _LOGGER.error("Failed to create async worker, error %s", exc) return None + return gateway diff --git a/homeassistant/components/snooz/strings.json b/homeassistant/components/snooz/strings.json index b38e105260c..5a31cea6cac 100644 --- a/homeassistant/components/snooz/strings.json +++ b/homeassistant/components/snooz/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 69e02c1875c..64f76372e91 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -4,13 +4,14 @@ from __future__ import annotations import socket -from requests.exceptions import ConnectTimeout, HTTPError -from solaredge import Solaredge +from aiohttp import ClientError +from aiosolaredge import SolarEdge from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER @@ -22,13 +23,12 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SolarEdge from a config entry.""" - api = Solaredge(entry.data[CONF_API_KEY]) + session = async_get_clientsession(hass) + api = SolarEdge(entry.data[CONF_API_KEY], session) try: - response = await hass.async_add_executor_job( - api.get_details, entry.data[CONF_SITE_ID] - ) - except (ConnectTimeout, HTTPError, socket.gaierror) as ex: + response = await api.get_details(entry.data[CONF_SITE_ID]) + except (TimeoutError, ClientError, socket.gaierror) as ex: LOGGER.error("Could not retrieve details from SolarEdge API") raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index b75af866549..6235e22400f 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -2,15 +2,17 @@ from __future__ import annotations +import socket from typing import Any -from requests.exceptions import ConnectTimeout, HTTPError -import solaredge +from aiohttp import ClientError +import aiosolaredge import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @@ -38,15 +40,16 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN): """Return True if site_id exists in configuration.""" return site_id in self._async_current_site_ids() - def _check_site(self, site_id: str, api_key: str) -> bool: + async def _async_check_site(self, site_id: str, api_key: str) -> bool: """Check if we can connect to the soleredge api service.""" - api = solaredge.Solaredge(api_key) + session = async_get_clientsession(self.hass) + api = aiosolaredge.SolarEdge(api_key, session) try: - response = api.get_details(site_id) + response = await api.get_details(site_id) if response["details"]["status"].lower() != "active": self._errors[CONF_SITE_ID] = "site_not_active" return False - except (ConnectTimeout, HTTPError): + except (TimeoutError, ClientError, socket.gaierror): self._errors[CONF_SITE_ID] = "could_not_connect" return False except KeyError: @@ -66,9 +69,7 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN): else: site = user_input[CONF_SITE_ID] api = user_input[CONF_API_KEY] - can_connect = await self.hass.async_add_executor_job( - self._check_site, site, api - ) + can_connect = await self._async_check_site(site, api) if can_connect: return self.async_create_entry( title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api} diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index d2da99820d7..0c264c1c514 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from datetime import date, datetime, timedelta from typing import Any -from solaredge import Solaredge +from aiosolaredge import SolarEdge from stringcase import snakecase from homeassistant.core import HomeAssistant, callback @@ -27,7 +27,7 @@ class SolarEdgeDataService(ABC): coordinator: DataUpdateCoordinator[None] - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id @@ -54,12 +54,8 @@ class SolarEdgeDataService(ABC): """Update interval.""" @abstractmethod - def update(self) -> None: - """Update data in executor.""" - async def async_update_data(self) -> None: """Update data.""" - await self.hass.async_add_executor_job(self.update) class SolarEdgeOverviewDataService(SolarEdgeDataService): @@ -70,10 +66,10 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): """Update interval.""" return OVERVIEW_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_overview(self.site_id) + data = await self.api.get_overview(self.site_id) overview = data["overview"] except KeyError as ex: raise UpdateFailed("Missing overview data, skipping update") from ex @@ -113,11 +109,11 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): """Update interval.""" return DETAILS_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_details(self.site_id) + data = await self.api.get_details(self.site_id) details = data["details"] except KeyError as ex: raise UpdateFailed("Missing details data, skipping update") from ex @@ -157,10 +153,10 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): """Update interval.""" return INVENTORY_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_inventory(self.site_id) + data = await self.api.get_inventory(self.site_id) inventory = data["Inventory"] except KeyError as ex: raise UpdateFailed("Missing inventory data, skipping update") from ex @@ -178,7 +174,7 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) @@ -189,17 +185,16 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Update interval.""" return ENERGY_DETAILS_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: now = datetime.now() today = date.today() midnight = datetime.combine(today, datetime.min.time()) - data = self.api.get_energy_details( + data = await self.api.get_energy_details( self.site_id, midnight, - now.strftime("%Y-%m-%d %H:%M:%S"), - meters=None, + now, time_unit="DAY", ) energy_details = data["energyDetails"] @@ -239,7 +234,7 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) @@ -250,10 +245,10 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Update interval.""" return POWER_FLOW_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_current_power_flow(self.site_id) + data = await self.api.get_current_power_flow(self.site_id) power_flow = data["siteCurrentPowerFlow"] except KeyError as ex: raise UpdateFailed("Missing power flow data, skipping update") from ex diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 22759b1be7c..02f96c0211f 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge", "name": "SolarEdge", - "codeowners": ["@frenck"], + "codeowners": ["@frenck", "@bdraco"], "config_flow": true, "dhcp": [ { @@ -12,6 +12,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge", "integration_type": "device", "iot_class": "cloud_polling", - "loggers": ["solaredge"], - "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"] + "loggers": ["aiosolaredge"], + "requirements": ["aiosolaredge==0.2.0", "stringcase==1.2.0"] } diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 5ec65a3b9a5..b3345d5dc86 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from solaredge import Solaredge +from aiosolaredge import SolarEdge from homeassistant.components.sensor import ( SensorDeviceClass, @@ -205,7 +205,7 @@ async def async_setup_entry( ) -> None: """Add an solarEdge entry.""" # Add the needed sensors to hass - api: Solaredge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] + api: SolarEdge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] sensor_factory = SolarEdgeSensorFactory(hass, entry.data[CONF_SITE_ID], api) for service in sensor_factory.all_services: @@ -223,7 +223,7 @@ async def async_setup_entry( class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" - def __init__(self, hass: HomeAssistant, site_id: str, api: Solaredge) -> None: + def __init__(self, hass: HomeAssistant, site_id: str, api: SolarEdge) -> None: """Initialize the factory.""" details = SolarEdgeDetailsDataService(hass, api, site_id) diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 83b9c600de8..40343b5ac12 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -44,14 +44,14 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): """Check if we can connect to the Solar-Log device.""" try: await self.hass.async_add_executor_job(SolarLog, host) - return True except (OSError, HTTPError, Timeout): self._errors[CONF_HOST] = "cannot_connect" _LOGGER.error( "Could not connect to Solar-Log device at %s, check host ip address", host, ) - return False + return False + return True async def async_step_user(self, user_input=None): """Step when user initializes a integration.""" diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index d3f677fa894..be81dd65e89 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/solax", "iot_class": "local_polling", "loggers": ["solax"], - "requirements": ["solax==0.3.2"] + "requirements": ["solax==3.1.0"] } diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index ccd1a8c96c9..a8c09bdc880 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -6,7 +6,7 @@ import asyncio from datetime import timedelta from solax import RealTimeAPI -from solax.discovery import InverterError +from solax.inverter import InverterError from solax.units import Units from homeassistant.components.sensor import ( diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index d4d33a77d43..c4dec6b938d 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["songpal"], "quality_scale": "gold", - "requirements": ["python-songpal==0.16.1"], + "requirements": ["python-songpal==0.16.2"], "ssdp": [ { "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 33dc65d5eaa..d3ce934ec51 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -11,9 +11,11 @@ from songpal import ( ContentChange, Device, PowerChange, + SettingChange, SongpalException, VolumeChange, ) +from songpal.containers import Setting import voluptuous as vol from homeassistant.components.media_player import ( @@ -99,6 +101,7 @@ class SongpalEntity(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.SELECT_SOUND_MODE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF ) @@ -124,6 +127,8 @@ class SongpalEntity(MediaPlayerEntity): self._active_source = None self._sources = {} + self._active_sound_mode = None + self._sound_modes = {} async def async_added_to_hass(self) -> None: """Run when entity is added to hass.""" @@ -133,6 +138,28 @@ class SongpalEntity(MediaPlayerEntity): """Run when entity will be removed from hass.""" await self._dev.stop_listen_notifications() + async def _get_sound_modes_info(self): + """Get available sound modes and the active one.""" + settings = await self._dev.get_sound_settings("soundField") + if isinstance(settings, Setting): + settings = [settings] + + sound_modes = {} + active_sound_mode = None + for setting in settings: + cur = setting.currentValue + for opt in setting.candidate: + if not opt.isAvailable: + continue + if opt.value == cur: + active_sound_mode = opt.value + sound_modes[opt.value] = opt + + _LOGGER.debug("Got sound modes: %s", sound_modes) + _LOGGER.debug("Active sound mode: %s", active_sound_mode) + + return active_sound_mode, sound_modes + async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" _LOGGER.info("Activating websocket connection") @@ -152,6 +179,16 @@ class SongpalEntity(MediaPlayerEntity): else: _LOGGER.debug("Got non-handled content change: %s", content) + async def _setting_changed(setting: SettingChange): + _LOGGER.debug("Setting changed: %s", setting) + + if setting.target == "soundField": + self._active_sound_mode = setting.currentValue + _LOGGER.debug("New active sound mode: %s", self._active_sound_mode) + self.async_write_ha_state() + else: + _LOGGER.debug("Got non-handled setting change: %s", setting) + async def _power_changed(power: PowerChange): _LOGGER.debug("Power changed: %s", power) self._state = power.status @@ -192,6 +229,7 @@ class SongpalEntity(MediaPlayerEntity): self._dev.on_notification(VolumeChange, _volume_changed) self._dev.on_notification(ContentChange, _source_changed) self._dev.on_notification(PowerChange, _power_changed) + self._dev.on_notification(SettingChange, _setting_changed) self._dev.on_notification(ConnectChange, _try_reconnect) async def handle_stop(event): @@ -271,6 +309,11 @@ class SongpalEntity(MediaPlayerEntity): _LOGGER.debug("Active source: %s", self._active_source) + ( + self._active_sound_mode, + self._sound_modes, + ) = await self._get_sound_modes_info() + self._attr_available = True except SongpalException as ex: @@ -291,6 +334,27 @@ class SongpalEntity(MediaPlayerEntity): """Return list of available sources.""" return [src.title for src in self._sources.values()] + async def async_select_sound_mode(self, sound_mode: str) -> None: + """Select sound mode.""" + for mode in self._sound_modes.values(): + if mode.title == sound_mode: + await self._dev.set_sound_settings("soundField", mode.value) + return + + _LOGGER.error("Unable to find sound mode: %s", sound_mode) + + @property + def sound_mode_list(self) -> list[str] | None: + """Return list of available sound modes. + + When active mode is None it means that sound mode is unavailable on the sound bar. + Can be due to incompatible sound bar or the sound bar is in a mode that does not + support sound mode changes. + """ + if not self._active_sound_mode: + return None + return [sound_mode.title for sound_mode in self._sound_modes.values()] + @property def state(self) -> MediaPlayerState: """Return current state.""" @@ -304,6 +368,12 @@ class SongpalEntity(MediaPlayerEntity): # Avoid a KeyError when _active_source is not (yet) populated return getattr(self._active_source, "title", None) + @property + def sound_mode(self) -> str | None: + """Return currently active sound_mode.""" + active_sound_mode = self._sound_modes.get(self._active_sound_mode) + return active_sound_mode.title if active_sound_mode else None + @property def volume_level(self): """Return volume level.""" diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 028c412cd75..2049cb4c8c7 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -230,7 +230,8 @@ class SonosDiscoveryManager: return self.hass.async_create_task( - self.async_add_speakers(zones_to_add, subscription, soco.uid) + self.async_add_speakers(zones_to_add, subscription, soco.uid), + eager_start=True, ) async def async_subscription_failed(now: datetime.datetime) -> None: @@ -576,7 +577,6 @@ class SonosDiscoveryManager: self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener, - run_immediately=True, ) ) _LOGGER.debug("Adding discovery job") @@ -585,7 +585,6 @@ class SonosDiscoveryManager: self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat, - run_immediately=True, ) ) await self.async_poll_manual_hosts() diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index b97b03b9be2..09fe9d9db5f 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -113,7 +113,7 @@ async def async_generate_speaker_info( payload: dict[str, Any] = {} def get_contents( - item: int | float | str | dict[str, Any], + item: float | str | dict[str, Any], ) -> int | float | str | dict[str, Any]: if isinstance(item, (int, float, str)): return item diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 58a0ec3b7ee..ec5ef90a0c1 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -2,13 +2,13 @@ "domain": "sonos", "name": "Sonos", "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], - "codeowners": ["@jjlawren"], + "codeowners": ["@jjlawren", "@peterager"], "config_flow": true, "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco"], - "requirements": ["soco==0.30.2", "sonos-websocket==0.1.3"], + "requirements": ["soco==0.30.3", "sonos-websocket==0.1.3"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 6e6f388ed50..eeadd7db232 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -199,9 +199,15 @@ def build_item_response( payload["search_type"] == MediaType.ALBUM and media[0].item_class == "object.item.audioItem.musicTrack" ): - item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST) + idstring = payload["idstring"] + if idstring.startswith("A:ALBUMARTIST/"): + search_type = SONOS_ALBUM_ARTIST + elif idstring.startswith("A:ALBUM/"): + search_type = SONOS_ALBUM + item = get_media(media_library, idstring, search_type) + title = getattr(item, "title", None) - thumbnail = get_thumbnail_url(SONOS_ALBUM_ARTIST, payload["idstring"]) + thumbnail = get_thumbnail_url(search_type, payload["idstring"]) if not title: try: @@ -493,15 +499,16 @@ def get_content_id(item: DidlObject) -> str: def get_media( media_library: MusicLibrary, item_id: str, search_type: str -) -> MusicServiceItem: - """Fetch media/album.""" +) -> MusicServiceItem | None: + """Fetch a single media/album.""" + _LOGGER.debug("get_media item_id [%s], search_type [%s]", item_id, search_type) 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( + return next( ( p for p in media_library.get_playlists() @@ -509,14 +516,42 @@ def get_media( ), 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 = 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 + if item_id.startswith("A:ALBUM/") or search_type == "tracks": + 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 + ) + else: + # When requesting media by album_artist, composer, genre use the browse interface + # to navigate the hierarchy. This occurs when invoked from media browser or service + # calls + # Example: A:ALBUMARTIST/Neil Young/Greatest Hits - get specific album + # Example: A:ALBUMARTIST/Neil Young - get all albums + # Others: composer, genre + # A:// + splits = item_id.split("/") + title = urllib.parse.unquote(splits[2]) if len(splits) > 2 else None + browse_id_string = splits[0] + "/" + splits[1] + matches = media_library.browse_by_idstring( + search_type, browse_id_string, full_album_art_uri=True + ) + if title: + result = next( + (item for item in matches if (title == item.title)), + None, + ) + matches = [result] + + _LOGGER.debug( + "get_media search_type [%s] item_id [%s] matches [%d]", + search_type, + item_id, + len(matches), ) if len(matches) > 0: return matches[0] + return None diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 581bdaad37d..35c6be3fa6b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -7,7 +7,7 @@ from functools import partial import logging from typing import Any -from soco import alarms +from soco import SoCo, alarms from soco.core import ( MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, @@ -15,6 +15,7 @@ from soco.core import ( PLAY_MODES, ) from soco.data_structures import DidlFavorite +from soco.ms_data_structures import MusicServiceItem from sonos_websocket.exception import SonosWebsocketError import voluptuous as vol @@ -549,6 +550,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, is_radio: bool, **kwargs: Any ) -> None: """Wrap sync calls to async_play_media.""" + _LOGGER.debug("_play_media media_type %s media_id %s", media_type, media_id) enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) if media_type == "favorite_item_id": @@ -645,10 +647,35 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): _LOGGER.error('Could not find "%s" in the library', media_id) return - soco.play_uri(item.get_uri()) + self._play_media_queue(soco, item, enqueue) else: _LOGGER.error('Sonos does not support a media type of "%s"', media_type) + def _play_media_queue( + self, soco: SoCo, item: MusicServiceItem, enqueue: MediaPlayerEnqueue + ): + """Manage adding, replacing, playing items onto the sonos queue.""" + _LOGGER.debug( + "_play_media_queue item_id [%s] title [%s] enqueue [%s]", + item.item_id, + item.title, + enqueue, + ) + if enqueue == MediaPlayerEnqueue.REPLACE: + soco.clear_queue() + + if enqueue in (MediaPlayerEnqueue.ADD, MediaPlayerEnqueue.REPLACE): + soco.add_to_queue(item, timeout=LONG_SERVICE_TIMEOUT) + if enqueue == MediaPlayerEnqueue.REPLACE: + soco.play_from_queue(0) + else: + pos = (self.media.queue_position or 0) + 1 + new_pos = soco.add_to_queue( + item, position=pos, timeout=LONG_SERVICE_TIMEOUT + ) + if enqueue == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) + @soco_error() def set_sleep_timer(self, sleep_time: int) -> None: """Set the timer on the player.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ebb2738c641..e2529ddfe94 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -407,7 +407,9 @@ class SonosSpeaker: @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" - self.hass.async_create_task(self._async_renew_failed(exception)) + self.hass.async_create_background_task( + self._async_renew_failed(exception), "sonos renew failed", eager_start=True + ) async def _async_renew_failed(self, exception: Exception) -> None: """Mark the speaker as offline after a subscription renewal failure. @@ -449,14 +451,20 @@ class SonosSpeaker: """Add the soco instance associated with the event to the callback.""" if "alarm_list_version" not in event.variables: return - self.hass.async_create_task(self.alarms.async_process_event(event, self)) + self.hass.async_create_background_task( + self.alarms.async_process_event(event, self), + "sonos process event", + eager_start=True, + ) @callback 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), eager_start=True + self.hass.async_create_background_task( + self.async_update_device_properties(event), + "sonos device properties", + eager_start=True, ) async def async_update_device_properties(self, event: SonosEvent) -> None: @@ -479,7 +487,11 @@ class SonosSpeaker: return if "container_update_i_ds" not in event.variables: return - self.hass.async_create_task(self.favorites.async_process_event(event, self)) + self.hass.async_create_background_task( + self.favorites.async_process_event(event, self), + "sonos dispatch favorites", + eager_start=True, + ) @callback def async_dispatch_media_update(self, event: SonosEvent) -> None: @@ -601,7 +613,7 @@ class SonosSpeaker: self.available = True if not was_available: self.async_write_entity_states() - self.hass.async_create_task(self.async_subscribe()) + self.hass.async_create_task(self.async_subscribe(), eager_start=True) @callback def async_check_activity(self, now: datetime.datetime) -> None: @@ -818,7 +830,9 @@ class SonosSpeaker: if "zone_player_uui_ds_in_group" not in event.variables: return self.event_stats.process(event) - self.hass.async_create_task(self.create_update_groups_coro(event)) + self.hass.async_create_task( + self.create_update_groups_coro(event), eager_start=True + ) def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: """Handle callback for topology change event.""" diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 0843cc1a826..c09c4ed72c4 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -409,10 +409,8 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): if slave_instance and slave_instance.entity_id != master: slaves.append(slave_instance.entity_id) - attributes = { + return { "master": master, "is_master": master == self.entity_id, "slaves": slaves, } - - return attributes diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 487e58d8f8b..2e725e8d139 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -101,8 +101,6 @@ def spotify_exception_handler( # pylint: disable=protected-access try: result = func(self, *args, **kwargs) - self._attr_available = True - return result except requests.RequestException: self._attr_available = False return None @@ -111,6 +109,8 @@ def spotify_exception_handler( if exc.reason == "NO_ACTIVE_DEVICE": raise HomeAssistantError("No active playback device found") from None raise HomeAssistantError(f"Spotify error: {exc.reason}") from exc + self._attr_available = True + return result return wrapper diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index b770a3e22a3..bc63bcb7f2f 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -170,8 +170,7 @@ async def library_payload(hass, player): else: library_info["children"].append(item) - response = BrowseMedia(**library_info) - return response + return BrowseMedia(**library_info) def media_source_content_filter(item: BrowseMedia) -> bool: diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d9478b6747d..a3a404fe1ae 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -253,14 +253,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): @property def extra_state_attributes(self): """Return device-specific attributes.""" - squeezebox_attr = { + return { attr: getattr(self, attr) for attr in ATTR_TO_PROPERTY if getattr(self, attr) is not None } - return squeezebox_attr - @callback def rediscovered(self, unique_id, connected): """Make a player available again.""" diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b34105106e0..1678daf4059 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -392,9 +392,7 @@ class Scanner: await self._async_start_ssdp_listeners() - 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) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL, name="SSDP scanner" ) @@ -735,10 +733,11 @@ async def _async_find_next_available_port(source: AddressTupleVXType) -> int: addr = (source[0],) + (port,) + source[2:] try: test_socket.bind(addr) - return port except OSError: if port == UPNP_SERVER_MAX_PORT - 1: raise + else: + return port raise RuntimeError("unreachable") @@ -754,13 +753,10 @@ 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, run_immediately=True - ) + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) 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/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index ff33b3ecc41..7a09b2f2dee 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -55,21 +55,21 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): update_interval=timedelta(seconds=5), ) + def _get_starlink_data(self) -> StarlinkData: + """Retrieve Starlink data.""" + channel_context = self.channel_context + status = status_data(channel_context) + location = location_data(channel_context) + sleep = get_sleep_config(channel_context) + return StarlinkData(location, sleep, *status) + async def _async_update_data(self) -> StarlinkData: async with asyncio.timeout(4): try: - status = await self.hass.async_add_executor_job( - status_data, self.channel_context - ) - location = await self.hass.async_add_executor_job( - location_data, self.channel_context - ) - sleep = await self.hass.async_add_executor_job( - get_sleep_config, self.channel_context - ) - return StarlinkData(location, sleep, *status) + result = await self.hass.async_add_executor_job(self._get_starlink_data) except GrpcError as exc: raise UpdateFailed from exc + return result async def async_stow_starlink(self, stow: bool) -> None: """Set whether Starlink system tied to this coordinator should be stowed.""" diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index d995f529b7d..713a8d3e894 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -35,6 +35,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -43,7 +44,6 @@ from homeassistant.core import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_utc_time, async_track_state_change_event, ) @@ -762,19 +762,18 @@ class StatisticsSensor(SensorEntity): def _stat_sum_differences(self) -> StateType: if len(self.states) >= 2: - diff_sum = sum( - abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:]) + return sum( + abs(j - i) + for i, j in zip(list(self.states), list(self.states)[1:], strict=False) ) - return diff_sum return None def _stat_sum_differences_nonnegative(self) -> StateType: if len(self.states) >= 2: - diff_sum_nn = sum( + return sum( (j - i if j >= i else j - 0) - for i, j in zip(list(self.states), list(self.states)[1:]) + for i, j in zip(list(self.states), list(self.states)[1:], strict=False) ) - return diff_sum_nn return None def _stat_total(self) -> StateType: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 44cf9177993..64c520150c2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -214,7 +214,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # 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, run_immediately=True + EVENT_LOGGING_CHANGED, update_pyav_logging ) # libav.mp4 and libav.swscaler have a few unimportant messages that are logged # at logging.WARNING. Set those Logger levels to logging.ERROR @@ -266,7 +266,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Stopped stream workers") cancel_logging_listener() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown, run_immediately=True) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) return True diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 87d9118f3a5..670d6b93c0e 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -583,9 +583,9 @@ def stream_worker( # dts. Use "or 1" to deal with this. start_dts = next_video_packet.dts - (next_video_packet.duration or 1) first_keyframe.dts = first_keyframe.pts = start_dts - except StreamWorkerError as ex: + except StreamWorkerError: container.close() - raise ex + raise except StopIteration as ex: container.close() raise StreamEndedError("Stream ended; no additional packets") from ex @@ -612,8 +612,8 @@ def stream_worker( while not quit_event.is_set(): try: packet = next(container_packets) - except StreamWorkerError as ex: - raise ex + except StreamWorkerError: + raise except StopIteration as ex: raise StreamEndedError("Stream ended; no additional packets") from ex except av.AVError as ex: diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index d7169fc181e..db2ee7fdbbc 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -149,7 +149,7 @@ async def update_subaru(vehicle, controller): def get_vehicle_info(controller, vin): """Obtain vehicle identifiers and capabilities.""" - info = { + return { VEHICLE_VIN: vin, VEHICLE_MODEL_NAME: controller.get_model_name(vin), VEHICLE_MODEL_YEAR: controller.get_model_year(vin), @@ -161,7 +161,6 @@ def get_vehicle_info(controller, vin): VEHICLE_HAS_SAFETY_SERVICE: controller.get_safety_status(vin), VEHICLE_LAST_UPDATE: 0, } - return info def get_device_info(vehicle_info): diff --git a/homeassistant/components/subaru/diagnostics.py b/homeassistant/components/subaru/diagnostics.py index 0a26387d1c2..726457aa341 100644 --- a/homeassistant/components/subaru/diagnostics.py +++ b/homeassistant/components/subaru/diagnostics.py @@ -25,7 +25,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.data, CONFIG_FIELDS_TO_REDACT), "options": async_redact_data(config_entry.options, []), "data": [ @@ -34,8 +34,6 @@ async def async_get_config_entry_diagnostics( ], } - return diagnostics_data - async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry diff --git a/homeassistant/components/suez_water/__init__.py b/homeassistant/components/suez_water/__init__.py index 07944de2c81..f5b2880e011 100644 --- a/homeassistant/components/suez_water/__init__.py +++ b/homeassistant/components/suez_water/__init__.py @@ -28,9 +28,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if not client.check_credentials(): raise ConfigEntryError - return client except PySuezError as ex: raise ConfigEntryNotReady from ex + return client hass.data.setdefault(DOMAIN, {})[ entry.entry_id diff --git a/homeassistant/components/sunweg/__init__.py b/homeassistant/components/sunweg/__init__.py index 6c39a04127e..86da0a247b1 100644 --- a/homeassistant/components/sunweg/__init__.py +++ b/homeassistant/components/sunweg/__init__.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.typing import StateType, UndefinedType from homeassistant.util import Throttle @@ -27,8 +28,7 @@ async def async_setup_entry( """Load the saved entities.""" api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) if not await hass.async_add_executor_job(api.authenticate): - _LOGGER.error("Username or Password may be incorrect!") - return False + raise ConfigEntryAuthFailed("Username or Password may be incorrect!") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData( api, entry.data[CONF_PLANT_ID] ) diff --git a/homeassistant/components/sunweg/config_flow.py b/homeassistant/components/sunweg/config_flow.py index c4af05a0cc9..2b5e49c2cb9 100644 --- a/homeassistant/components/sunweg/config_flow.py +++ b/homeassistant/components/sunweg/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Sun WEG integration.""" -from sunweg.api import APIHelper +from collections.abc import Mapping +from typing import Any + +from sunweg.api import APIHelper, SunWegApiError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -18,37 +21,61 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialise sun weg server flow.""" self.api: APIHelper = None - self.data: dict = {} + self.data: dict[str, Any] = {} @callback - def _async_show_user_form(self, errors=None) -> ConfigFlowResult: + def _async_show_user_form(self, step_id: str, errors=None) -> ConfigFlowResult: """Show the form to the user.""" + default_username = "" + if CONF_USERNAME in self.data: + default_username = self.data[CONF_USERNAME] data_schema = vol.Schema( { - vol.Required(CONF_USERNAME): str, + vol.Required(CONF_USERNAME, default=default_username): str, vol.Required(CONF_PASSWORD): str, } ) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id=step_id, data_schema=data_schema, errors=errors ) + def _set_auth_data( + self, step: str, username: str, password: str + ) -> ConfigFlowResult | None: + """Set username and password.""" + if self.api: + # Set username and password + self.api.username = username + self.api.password = password + else: + # Initialise the library with the username & password + self.api = APIHelper(username, password) + + try: + if not self.api.authenticate(): + return self._async_show_user_form(step, {"base": "invalid_auth"}) + except SunWegApiError: + return self._async_show_user_form(step, {"base": "timeout_connect"}) + + return None + 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() - - # Initialise the library with the username & password - self.api = APIHelper(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) - login_response = await self.hass.async_add_executor_job(self.api.authenticate) - - if not login_response: - return self._async_show_user_form({"base": "invalid_auth"}) + return self._async_show_user_form("user") # Store authentication info self.data = user_input - return await self.async_step_plant() + + conf_result = await self.hass.async_add_executor_job( + self._set_auth_data, + "user", + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + return await self.async_step_plant() if conf_result is None else conf_result async def async_step_plant(self, user_input=None) -> ConfigFlowResult: """Handle adding a "plant" to Home Assistant.""" @@ -72,3 +99,37 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data.update(user_input) return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauthorization request from SunWEG.""" + self.data.update(entry_data) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauthorization flow.""" + if user_input is None: + return self._async_show_user_form("reauth_confirm") + + self.data.update(user_input) + conf_result = await self.hass.async_add_executor_job( + self._set_auth_data, + "reauth_confirm", + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + if conf_result is not None: + return conf_result + + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if entry is not None: + data: Mapping[str, Any] = self.data + self.hass.config_entries.async_update_entry(entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/sunweg/strings.json b/homeassistant/components/sunweg/strings.json index 3a910e62940..6033bc314bc 100644 --- a/homeassistant/components/sunweg/strings.json +++ b/homeassistant/components/sunweg/strings.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "no_plants": "No plants have been found on this account" + "no_plants": "No plants have been found on this account", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" }, "step": { "plant": { @@ -19,6 +21,13 @@ "username": "[%key:common::config_flow::data::username%]" }, "title": "Enter your Sun WEG information" + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "title": "[%key:common::config_flow::title::reauth%]" } } } diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 46a3ec2b2c0..8f04b5b662e 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -101,13 +101,12 @@ async def discover_devices(hass, hass_config): async def _fetch_channels(): async with asyncio.timeout(SCAN_INTERVAL.total_seconds()): - channels = { + return { channel["id"]: channel for channel in await server.get_channels( # noqa: B023 include=["iodevice", "state", "connected"] ) } - return channels coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index 7df593d5667..eb6ab9c6017 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -25,16 +25,24 @@ class DataConnection(TypedDict): departure: datetime | None next_departure: datetime | None next_on_departure: datetime | None - duration: str + duration: int | None platform: str remaining_time: str start: str destination: str train_number: str - transfers: str + transfers: int delay: int +def calculate_duration_in_seconds(duration_text: str) -> int | None: + """Transform and calculate the duration into seconds.""" + # Transform 01d03:21:23 into 01 days 03:21:23 + duration_text_pg_format = duration_text.replace("d", " days ") + duration = dt_util.parse_duration(duration_text_pg_format) + return duration.seconds if duration else None + + class SwissPublicTransportDataUpdateCoordinator( DataUpdateCoordinator[list[DataConnection]] ): @@ -77,7 +85,6 @@ class SwissPublicTransportDataUpdateCoordinator( raise UpdateFailed from e connections = self._opendata.connections - return [ DataConnection( departure=self.nth_departure_time(i), @@ -86,7 +93,7 @@ class SwissPublicTransportDataUpdateCoordinator( train_number=connections[i]["number"], platform=connections[i]["platform"], transfers=connections[i]["transfers"], - duration=connections[i]["duration"], + duration=calculate_duration_in_seconds(connections[i]["duration"]), start=self._opendata.from_name, destination=self._opendata.to_name, remaining_time=str(self.remaining_time(connections[i]["departure"])), diff --git a/homeassistant/components/swiss_public_transport/icons.json b/homeassistant/components/swiss_public_transport/icons.json index fac54b10809..10573b8f5c3 100644 --- a/homeassistant/components/swiss_public_transport/icons.json +++ b/homeassistant/components/swiss_public_transport/icons.json @@ -1,8 +1,26 @@ { "entity": { "sensor": { - "departure": { - "default": "mdi:bus" + "departure0": { + "default": "mdi:bus-clock" + }, + "departure1": { + "default": "mdi:bus-clock" + }, + "departure2": { + "default": "mdi:bus-clock" + }, + "duration": { + "default": "mdi:timeline-clock" + }, + "transfers": { + "default": "mdi:transit-transfer" + }, + "platform": { + "default": "mdi:bus-stop-uncovered" + }, + "delay": { + "default": "mdi:clock-plus" } } } diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index a4a9605a603..f477c04f6ec 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -18,14 +18,14 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback 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.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -55,11 +55,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( class SwissPublicTransportSensorEntityDescription(SensorEntityDescription): """Describes swiss public transport sensor entity.""" - exists_fn: Callable[[DataConnection], bool] - value_fn: Callable[[DataConnection], datetime | None] + value_fn: Callable[[DataConnection], StateType | datetime] - index: int - has_legacy_attributes: bool + index: int = 0 + has_legacy_attributes: bool = False SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( @@ -70,11 +69,33 @@ SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( 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) ], + SwissPublicTransportSensorEntityDescription( + key="duration", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=lambda data_connection: data_connection["duration"], + ), + SwissPublicTransportSensorEntityDescription( + key="transfers", + translation_key="transfers", + value_fn=lambda data_connection: data_connection["transfers"], + ), + SwissPublicTransportSensorEntityDescription( + key="platform", + translation_key="platform", + value_fn=lambda data_connection: data_connection["platform"], + ), + SwissPublicTransportSensorEntityDescription( + key="delay", + translation_key="delay", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + value_fn=lambda data_connection: data_connection["delay"], + ), ) @@ -167,14 +188,7 @@ class SwissPublicTransportSensor( ) @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: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn( self.coordinator.data[self.entity_description.index] @@ -196,10 +210,11 @@ class SwissPublicTransportSensor( @callback def _async_update_attrs(self) -> None: """Update the extra state attributes based on the coordinator data.""" - self._attr_extra_state_attributes = { - key: value - for key, value in self.coordinator.data[ - self.entity_description.index - ].items() - if key not in {"departure"} - } + if self.entity_description.has_legacy_attributes: + self._attr_extra_state_attributes = { + key: value + for key, value in self.coordinator.data[ + self.entity_description.index + ].items() + if key not in {"departure"} + } diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index c080e785f2c..cddc732d3ed 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -32,13 +32,25 @@ }, "departure2": { "name": "Departure +2" + }, + "duration": { + "name": "Duration" + }, + "transfers": { + "name": "Transfers" + }, + "platform": { + "name": "Platform" + }, + "delay": { + "name": "Delay" } } }, "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 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." + "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/switch/__init__.py b/homeassistant/components/switch/__init__.py index 86c67248eea..995bcda294f 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,9 +4,8 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING import voluptuous as vol @@ -35,11 +34,6 @@ from homeassistant.loader import bind_hass from .const import DOMAIN -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 25214822bdb..f226ed57e2a 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -16,14 +16,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er 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.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN as SWITCH_DOMAIN diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index 9d03965a242..7c6a7ff38ad 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -18,10 +18,9 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import EventStateChangedData from .const import CONF_INVERT from .entity import BaseInvertableEntity diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index e8e57570617..020d92e21ac 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -13,14 +13,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, 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 -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from .const import DOMAIN as SWITCH_AS_X_DOMAIN diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index 5243ae184ee..2095b06bd84 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -14,10 +14,9 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import EventStateChangedData from .const import CONF_INVERT from .entity import BaseInvertableEntity diff --git a/homeassistant/components/switch_as_x/valve.py b/homeassistant/components/switch_as_x/valve.py index 98f0e52c8a2..8626ca3cfb4 100644 --- a/homeassistant/components/switch_as_x/valve.py +++ b/homeassistant/components/switch_as_x/valve.py @@ -18,10 +18,9 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import EventStateChangedData from .const import CONF_INVERT from .entity import BaseInvertableEntity diff --git a/homeassistant/components/switchbot_cloud/manifest.json b/homeassistant/components/switchbot_cloud/manifest.json index cb651e5c84f..2b50f39925f 100644 --- a/homeassistant/components/switchbot_cloud/manifest.json +++ b/homeassistant/components/switchbot_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot_cloud", "iot_class": "cloud_polling", "loggers": ["switchbot-api"], - "requirements": ["switchbot-api==2.0.0"] + "requirements": ["switchbot-api==2.1.0"] } diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 5ad4a85cc09..c6764de51a7 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: printer.url, exc_info=api_error, ) - raise api_error + raise # if the printer is offline, we raise an UpdateFailed if printer.is_unknown_state(): diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ec93c92a698..2748b27c93d 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -11,10 +11,10 @@ from synology_dsm.api.surveillance_station.camera import SynoCamera from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from .common import SynoApi +from .common import SynoApi, raise_config_entry_auth_error from .const import ( DEFAULT_VERIFY_SSL, DOMAIN, @@ -68,11 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await api.async_setup() except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: - if err.args[0] and isinstance(err.args[0], dict): - details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) - else: - details = EXCEPTION_UNKNOWN - raise ConfigEntryAuthFailed(f"reason: {details}") from err + raise_config_entry_auth_error(err) except SYNOLOGY_CONNECTION_EXCEPTIONS as err: if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) @@ -147,8 +143,10 @@ async def async_remove_config_entry_device( """Remove synology_dsm config entry from a device.""" data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api + assert api.information is not None serial = api.information.serial storage = api.storage + assert storage is not None all_cameras: list[SynoCamera] = [] if api.surveillance_station is not None: # get_all_cameras does not do I/O @@ -163,6 +161,8 @@ async def async_remove_config_entry_device( return not device_entry.identifiers.intersection( ( (DOMAIN, serial), # Base device - *((DOMAIN, f"{serial}_{id}") for id in device_ids), # Storage and cameras + *( + (DOMAIN, f"{serial}_{device_id}") for device_id in device_ids + ), # Storage and cameras ) ) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 28dc750bc91..b9c7ff483ea 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -69,6 +69,7 @@ async def async_setup_entry( data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api coordinator = data.coordinator_central + assert api.storage is not None entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [ SynoDSMSecurityBinarySensor(api, coordinator, description) @@ -121,7 +122,8 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor): @property def extra_state_attributes(self) -> dict[str, str]: """Return security checks details.""" - return self._api.security.status_by_check # type: ignore[no-any-return] + assert self._api.security is not None + return self._api.security.status_by_check class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, SynoDSMBinarySensor): diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 529682b4c6e..fccd0860036 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -73,7 +73,8 @@ class SynologyDSMButton(ButtonEntity): """Initialize the Synology DSM binary_sensor entity.""" self.entity_description = description self.syno_api = api - + assert api.network is not None + assert api.information is not None self._attr_name = f"{api.network.hostname} {description.name}" self._attr_unique_id = f"{api.information.serial}_{description.key}" self._attr_device_info = DeviceInfo( @@ -82,6 +83,7 @@ class SynologyDSMButton(ButtonEntity): async def async_press(self) -> None: """Triggers the Synology DSM button press service.""" + assert self.syno_api.network is not None LOGGER.debug( "Trigger %s for %s", self.entity_description.key, diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 82d15138f05..1d03fd4f027 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -42,6 +42,8 @@ class SynologyDSMCameraEntityDescription( ): """Describes Synology DSM camera entity.""" + camera_id: int + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -65,13 +67,14 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C self, api: SynoApi, coordinator: SynologyDSMCameraUpdateCoordinator, - camera_id: str, + camera_id: int, ) -> None: """Initialize a Synology camera.""" description = SynologyDSMCameraEntityDescription( api_key=SynoSurveillanceStation.CAMERA_API_KEY, - key=camera_id, - name=coordinator.data["cameras"][camera_id].name, + key=str(camera_id), + camera_id=camera_id, + name=None, entity_registry_enabled_default=coordinator.data["cameras"][ camera_id ].is_enabled, @@ -85,23 +88,20 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def camera_data(self) -> SynoCamera: """Camera data.""" - return self.coordinator.data["cameras"][self.entity_description.key] + return self.coordinator.data["cameras"][self.entity_description.camera_id] @property def device_info(self) -> DeviceInfo: """Return the device information.""" + information = self._api.information + assert information is not None return DeviceInfo( - identifiers={ - ( - DOMAIN, - f"{self._api.information.serial}_{self.camera_data.id}", - ) - }, + identifiers={(DOMAIN, f"{information.serial}_{self.camera_data.id}")}, name=self.camera_data.name, model=self.camera_data.model, via_device=( DOMAIN, - f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", + f"{information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ), ) @@ -113,12 +113,12 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.camera_data.is_recording # type: ignore[no-any-return] + return self.camera_data.is_recording @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + return bool(self.camera_data.is_motion_detection_enabled) def _listen_source_updates(self) -> None: """Listen for camera source changed events.""" @@ -153,9 +153,10 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C ) if not self.available: return None + assert self._api.surveillance_station is not None try: - return await self._api.surveillance_station.get_camera_image( # type: ignore[no-any-return] - self.entity_description.key, self.snapshot_quality + return await self._api.surveillance_station.get_camera_image( + self.entity_description.camera_id, self.snapshot_quality ) except ( SynologyDSMAPIErrorException, @@ -178,7 +179,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C if not self.available: return None - return self.camera_data.live_view.rtsp # type: ignore[no-any-return] + return self.camera_data.live_view.rtsp async def async_enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" @@ -186,8 +187,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C "SynoDSMCamera.enable_motion_detection(%s)", self.camera_data.name, ) + assert self._api.surveillance_station is not None await self._api.surveillance_station.enable_motion_detection( - self.entity_description.key + self.entity_description.camera_id ) async def async_disable_motion_detection(self) -> None: @@ -196,6 +198,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C "SynoDSMCamera.disable_motion_detection(%s)", self.camera_data.name, ) + assert self._api.surveillance_station is not None await self._api.surveillance_station.disable_motion_detection( - self.entity_description.key + self.entity_description.camera_id ) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 4a7018119be..04e8ae29ceb 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -33,9 +33,15 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_DEVICE_TOKEN, SYNOLOGY_CONNECTION_EXCEPTIONS +from .const import ( + CONF_DEVICE_TOKEN, + EXCEPTION_DETAILS, + EXCEPTION_UNKNOWN, + SYNOLOGY_CONNECTION_EXCEPTIONS, +) LOGGER = logging.getLogger(__name__) @@ -43,6 +49,8 @@ LOGGER = logging.getLogger(__name__) class SynoApi: """Class to interface with Synology DSM API.""" + dsm: SynologyDSM + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the API wrapper class.""" self._hass = hass @@ -53,16 +61,15 @@ class SynoApi: self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" # DSM APIs - self.dsm: SynologyDSM = None - self.information: SynoDSMInformation = None - self.network: SynoDSMNetwork = None - self.security: SynoCoreSecurity = None - self.storage: SynoStorage = None - self.photos: SynoPhotos = None - self.surveillance_station: SynoSurveillanceStation = None - self.system: SynoCoreSystem = None - self.upgrade: SynoCoreUpgrade = None - self.utilisation: SynoCoreUtilization = None + self.information: SynoDSMInformation | None = None + self.network: SynoDSMNetwork | None = None + self.security: SynoCoreSecurity | None = None + self.storage: SynoStorage | None = None + self.photos: SynoPhotos | None = None + self.surveillance_station: SynoSurveillanceStation | None = None + self.system: SynoCoreSystem | None = None + self.upgrade: SynoCoreUpgrade | None = None + self.utilisation: SynoCoreUtilization | None = None # Should we fetch them self._fetching_entities: dict[str, set[str]] = {} @@ -85,7 +92,7 @@ class SynoApi: self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - timeout=self._entry.options.get(CONF_TIMEOUT), + timeout=self._entry.options.get(CONF_TIMEOUT) or 10, device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) await self.dsm.login() @@ -129,7 +136,7 @@ class SynoApi: self._entry.unique_id, err, ) - raise err + raise @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -159,7 +166,8 @@ class SynoApi: return # surveillance_station is updated by own coordinator - self.dsm.reset(self.surveillance_station) + if self.surveillance_station: + self.dsm.reset(self.surveillance_station) # Determine if we should fetch an API self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) @@ -182,35 +190,40 @@ class SynoApi: "Disable security api from being updated for '%s'", self._entry.unique_id, ) - self.dsm.reset(self.security) + if self.security: + self.dsm.reset(self.security) self.security = None if not self._with_photos: LOGGER.debug( "Disable photos api from being updated or '%s'", self._entry.unique_id ) - self.dsm.reset(self.photos) + if self.photos: + self.dsm.reset(self.photos) self.photos = None if not self._with_storage: LOGGER.debug( "Disable storage api from being updatedf or '%s'", self._entry.unique_id ) - self.dsm.reset(self.storage) + if self.storage: + self.dsm.reset(self.storage) self.storage = None if not self._with_system: LOGGER.debug( "Disable system api from being updated for '%s'", self._entry.unique_id ) - self.dsm.reset(self.system) + if self.system: + self.dsm.reset(self.system) self.system = None if not self._with_upgrade: LOGGER.debug( "Disable upgrade api from being updated for '%s'", self._entry.unique_id ) - self.dsm.reset(self.upgrade) + if self.upgrade: + self.dsm.reset(self.upgrade) self.upgrade = None if not self._with_utilisation: @@ -218,7 +231,8 @@ class SynoApi: "Disable utilisation api from being updated for '%s'", self._entry.unique_id, ) - self.dsm.reset(self.utilisation) + if self.utilisation: + self.dsm.reset(self.utilisation) self.utilisation = None async def _fetch_device_configuration(self) -> None: @@ -268,15 +282,17 @@ class SynoApi: LOGGER.debug( "Error from '%s': %s", self._entry.unique_id, err, exc_info=True ) - raise err + raise async def async_reboot(self) -> None: """Reboot NAS.""" - await self._syno_api_executer(self.system.reboot) + if self.system: + await self._syno_api_executer(self.system.reboot) async def async_shutdown(self) -> None: """Shutdown NAS.""" - await self._syno_api_executer(self.system.shutdown) + if self.system: + await self._syno_api_executer(self.system.shutdown) async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" @@ -293,3 +309,12 @@ class SynoApi: LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._setup_api_requests() await self.dsm.update(self._with_information) + + +def raise_config_entry_auth_error(err: Exception) -> None: + """Raise ConfigEntryAuthFailed if error is related to authentication.""" + if err.args[0] and isinstance(err.args[0], dict): + details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) + else: + details = EXCEPTION_UNKNOWN + raise ConfigEntryAuthFailed(f"reason: {details}") from err diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index c77b8196faf..785baa50b29 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -425,7 +425,7 @@ async def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> ): raise InvalidData - return api.information.serial # type: ignore[no-any-return] + return api.information.serial class InvalidData(HomeAssistantError): diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index bc896b1ad45..34886828a58 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -7,7 +7,10 @@ import logging from typing import Any, TypeVar from synology_dsm.api.surveillance_station.camera import SynoCamera -from synology_dsm.exceptions import SynologyDSMAPIErrorException +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMNotLoggedInException, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL @@ -15,10 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .common import SynoApi +from .common import SynoApi, raise_config_entry_auth_error from .const import ( DEFAULT_SCAN_INTERVAL, SIGNAL_CAMERA_SOURCE_CHANGED, + SYNOLOGY_AUTH_FAILED_EXCEPTIONS, SYNOLOGY_CONNECTION_EXCEPTIONS, ) @@ -65,13 +69,17 @@ class SynologyDSMSwitchUpdateCoordinator( async def async_setup(self) -> None: """Set up the coordinator initial data.""" info = await self.api.dsm.surveillance_station.get_info() + assert info is not None self.version = info["data"]["CMSMinVersion"] async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station + assert surveillance_station is not None return { - "switches": {"home_mode": await surveillance_station.get_home_mode_status()} + "switches": { + "home_mode": bool(await surveillance_station.get_home_mode_status()) + } } @@ -96,14 +104,23 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Fetch all data from api.""" - try: - await self.api.async_update() - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + for attempts in range(2): + try: + await self.api.async_update() + except SynologyDSMNotLoggedInException: + # If login is expired, try to login again + try: + await self.api.dsm.login() + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: + raise_config_entry_auth_error(err) + if attempts == 0: + continue + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err class SynologyDSMCameraUpdateCoordinator( - SynologyDSMUpdateCoordinator[dict[str, dict[str, SynoCamera]]] + SynologyDSMUpdateCoordinator[dict[str, dict[int, SynoCamera]]] ): """DataUpdateCoordinator to gather data for a synology_dsm cameras.""" @@ -116,10 +133,11 @@ class SynologyDSMCameraUpdateCoordinator( """Initialize DataUpdateCoordinator for cameras.""" super().__init__(hass, entry, api, timedelta(seconds=30)) - async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]]: + async def _async_update_data(self) -> dict[str, dict[int, SynoCamera]]: """Fetch all camera data from api.""" surveillance_station = self.api.surveillance_station - current_data: dict[str, SynoCamera] = { + assert surveillance_station is not None + current_data: dict[int, SynoCamera] = { camera.id: camera for camera in surveillance_station.get_all_cameras() } @@ -128,7 +146,7 @@ class SynologyDSMCameraUpdateCoordinator( except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - new_data: dict[str, SynoCamera] = { + new_data: dict[int, SynoCamera] = { camera.id: camera for camera in surveillance_station.get_all_cameras() } diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index d9b4131b078..42a8ab8d60f 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from synology_dsm.api.surveillance_station.camera import SynoCamera - from homeassistant.components.camera import diagnostics as camera_diagnostics from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -47,7 +45,6 @@ async def async_get_config_entry_diagnostics( } if syno_api.network is not None: - intf: dict for intf in syno_api.network.interfaces: diag_data["network"]["interfaces"][intf["id"]] = { "type": intf["type"], @@ -55,7 +52,6 @@ async def async_get_config_entry_diagnostics( } if syno_api.storage is not None: - disk: dict for disk in syno_api.storage.disks: diag_data["storage"]["disks"][disk["id"]] = { "name": disk["name"], @@ -66,7 +62,6 @@ async def async_get_config_entry_diagnostics( "size_total": disk["size_total"], } - volume: dict for volume in syno_api.storage.volumes: diag_data["storage"]["volumes"][volume["id"]] = { "name": volume["fs_type"], @@ -74,7 +69,6 @@ async def async_get_config_entry_diagnostics( } if syno_api.surveillance_station is not None: - camera: SynoCamera for camera in syno_api.surveillance_station.get_all_cameras(): diag_data["surveillance_station"]["cameras"][camera.id] = { "name": camera.name, diff --git a/homeassistant/components/synology_dsm/entity.py b/homeassistant/components/synology_dsm/entity.py index 4bd1e526194..1a2e07af9e1 100644 --- a/homeassistant/components/synology_dsm/entity.py +++ b/homeassistant/components/synology_dsm/entity.py @@ -45,16 +45,21 @@ class SynologyDSMBaseEntity(CoordinatorEntity[_CoordinatorT]): self.entity_description = description self._api = api + information = api.information + network = api.network + assert information is not None + assert network is not None + self._attr_unique_id: str = ( - f"{api.information.serial}_{description.api_key}:{description.key}" + f"{information.serial}_{description.api_key}:{description.key}" ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._api.information.serial)}, - name=self._api.network.hostname, + identifiers={(DOMAIN, information.serial)}, + name=network.hostname, manufacturer="Synology", - model=self._api.information.model, - sw_version=self._api.information.version_string, - configuration_url=self._api.config_url, + model=information.model, + sw_version=information.version_string, + configuration_url=api.config_url, ) async def async_added_to_hass(self) -> None: @@ -85,14 +90,22 @@ class SynologyDSMDeviceEntity( self._device_model: str | None = None self._device_firmware: str | None = None self._device_type = None + storage = api.storage + information = api.information + network = api.network + assert information is not None + assert storage is not None + assert network is not None if "volume" in description.key: - volume = self._api.storage.get_volume(self._device_id) + assert self._device_id is not None + volume = storage.get_volume(self._device_id) + assert volume is not None # Volume does not have a name self._device_name = volume["id"].replace("_", " ").capitalize() self._device_manufacturer = "Synology" - self._device_model = self._api.information.model - self._device_firmware = self._api.information.version_string + self._device_model = information.model + self._device_firmware = information.version_string self._device_type = ( volume["device_type"] .replace("_", " ") @@ -100,7 +113,9 @@ class SynologyDSMDeviceEntity( .replace("shr", "SHR") ) elif "disk" in description.key: - disk = self._api.storage.get_disk(self._device_id) + assert self._device_id is not None + disk = storage.get_disk(self._device_id) + assert disk is not None self._device_name = disk["name"] self._device_manufacturer = disk["vendor"] self._device_model = disk["model"].strip() @@ -109,11 +124,11 @@ class SynologyDSMDeviceEntity( self._attr_unique_id += f"_{self._device_id}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._api.information.serial}_{self._device_id}")}, - name=f"{self._api.network.hostname} ({self._device_name})", + identifiers={(DOMAIN, f"{information.serial}_{self._device_id}")}, + name=f"{network.hostname} ({self._device_name})", manufacturer=self._device_manufacturer, model=self._device_model, sw_version=self._device_firmware, - via_device=(DOMAIN, self._api.information.serial), + via_device=(DOMAIN, information.serial), configuration_url=self._api.config_url, ) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 8060bce5c9b..caecfcbd0c9 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.1.4"], + "requirements": ["py-synologydsm-api==2.4.2"], "ssdp": [ { "manufacturer": "Synology", diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 9a393813c3e..4699a1a5c20 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -105,6 +105,7 @@ class SynologyPhotosMediaSource(MediaSource): ] identifier = SynologyPhotosMediaSourceIdentifier(item.identifier) diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id] + assert diskstation.api.photos is not None if identifier.album_id is None: # Get Albums @@ -112,6 +113,7 @@ class SynologyPhotosMediaSource(MediaSource): albums = await diskstation.api.photos.get_albums() except SynologyDSMException: return [] + assert albums is not None ret = [ BrowseMediaSource( @@ -148,6 +150,7 @@ class SynologyPhotosMediaSource(MediaSource): ) except SynologyDSMException: return [] + assert album_items is not None ret = [] for album_item in album_items: @@ -190,6 +193,8 @@ class SynologyPhotosMediaSource(MediaSource): self, item: SynoPhotosItem, diskstation: SynologyDSMData ) -> str | None: """Get thumbnail.""" + assert diskstation.api.photos is not None + try: thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item) except SynologyDSMException: @@ -215,13 +220,14 @@ class SynologyDsmMediaView(http.HomeAssistantView): raise web.HTTPNotFound # location: {cache_key}/{filename} cache_key, file_name = location.split("/") - image_id = cache_key.split("_")[0] + image_id = int(cache_key.split("_")[0]) mime_type, _ = mimetypes.guess_type(file_name) if not isinstance(mime_type, str): raise web.HTTPNotFound diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id] - item = SynoPhotosItem(image_id, "", "", "", cache_key, "") + assert diskstation.api.photos is not None + item = SynoPhotosItem(image_id, "", "", "", cache_key, "", False) try: image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 4f20a6233f3..b29a33f7253 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta +from typing import cast from synology_dsm.api.core.utilization import SynoCoreUtilization from synology_dsm.api.dsm.information import SynoDSMInformation @@ -291,6 +292,8 @@ async def async_setup_entry( data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api coordinator = data.coordinator_central + storage = api.storage + assert storage is not None entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ SynoDSMUtilSensor(api, coordinator, description) @@ -298,21 +301,21 @@ async def async_setup_entry( ] # Handle all volumes - if api.storage.volumes_ids: + if storage.volumes_ids: entities.extend( [ SynoDSMStorageSensor(api, coordinator, description, volume) - for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids) + for volume in entry.data.get(CONF_VOLUMES, storage.volumes_ids) for description in STORAGE_VOL_SENSORS ] ) # Handle all disks - if api.storage.disks_ids: + if storage.disks_ids: entities.extend( [ SynoDSMStorageSensor(api, coordinator, description, disk) - for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids) + for disk in entry.data.get(CONF_DISKS, storage.disks_ids) for description in STORAGE_DISK_SENSORS ] ) @@ -387,8 +390,10 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor): @property def native_value(self) -> StateType: """Return the state.""" - attr = getattr(self._api.storage, self.entity_description.key)(self._device_id) - return attr # type: ignore[no-any-return] + return cast( + StateType, + getattr(self._api.storage, self.entity_description.key)(self._device_id), + ) class SynoDSMInfoSensor(SynoDSMSensor): diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index c19cdb8c815..facce824bda 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -79,6 +79,8 @@ class SynoDSMSurveillanceHomeModeToggle( async def async_turn_on(self, **kwargs: Any) -> None: """Turn on Home mode.""" + assert self._api.surveillance_station is not None + assert self._api.information _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", self._api.information.serial, @@ -88,6 +90,8 @@ class SynoDSMSurveillanceHomeModeToggle( async def async_turn_off(self, **kwargs: Any) -> None: """Turn off Home mode.""" + assert self._api.surveillance_station is not None + assert self._api.information _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", self._api.information.serial, @@ -103,6 +107,9 @@ class SynoDSMSurveillanceHomeModeToggle( @property def device_info(self) -> DeviceInfo: """Return the device information.""" + assert self._api.surveillance_station is not None + assert self._api.information is not None + assert self._api.network is not None return DeviceInfo( identifiers={ ( diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index c7bcff48cea..ed60191f296 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -64,24 +64,29 @@ class SynoDSMUpdateEntity( @property def installed_version(self) -> str | None: """Version installed and in use.""" - return self._api.information.version_string # type: ignore[no-any-return] + assert self._api.information is not None + return self._api.information.version_string @property def latest_version(self) -> str | None: """Latest version available for install.""" + assert self._api.upgrade is not None if not self._api.upgrade.update_available: return self.installed_version - return self._api.upgrade.available_version # type: ignore[no-any-return] + return self._api.upgrade.available_version @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" + assert self._api.information is not None + assert self._api.upgrade is not None + if (details := self._api.upgrade.available_version_details) is None: return None url = URL("http://update.synology.com/autoupdate/whatsnew.php") query = {"model": self._api.information.model} - if details.get("nano") > 0: + if details["nano"] > 0: query["update_version"] = f"{details['buildnumber']}-{details['nano']}" else: query["update_version"] = details["buildnumber"] diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 6a1e4830443..bb050d5052e 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -127,6 +127,7 @@ async def handle_info( for registration in registrations.values() ) ), + strict=False, ): for key, value in domain_data["info"].items(): if asyncio.iscoroutine(value): @@ -235,11 +236,12 @@ async def async_check_can_reach_url( try: await session.get(url, timeout=5) - return "ok" except aiohttp.ClientError: data = {"type": "failed", "error": "unreachable"} except TimeoutError: data = {"type": "failed", "error": "timeout"} + else: + return "ok" if more_info is not None: data["more_info"] = more_info return data diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 423f5c6f5d8..b7222b75b72 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -7,6 +7,7 @@ import logging import re import sys import traceback +from types import FrameType from typing import Any, cast import voluptuous as vol @@ -18,7 +19,7 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -KeyType = tuple[str, tuple[str, int], str | None] +KeyType = tuple[str, tuple[str, int], tuple[str, int, str] | None] CONF_MAX_ENTRIES = "max_entries" CONF_FIRE_EVENT = "fire_event" @@ -65,16 +66,18 @@ SERVICE_WRITE_SCHEMA = vol.Schema( def _figure_out_source( record: logging.LogRecord, paths_re: re.Pattern[str], - extracted_tb: traceback.StackSummary | None = None, + extracted_tb: list[tuple[FrameType, int]] | 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: + source: list[tuple[FrameType, int]] = extracted_tb or list( + traceback.walk_tb(record.exc_info[2]) + ) stack = [ - (x[0], x[1]) - for x in (extracted_tb or traceback.extract_tb(record.exc_info[2])) + (tb_frame.f_code.co_filename, tb_line_no) for tb_frame, tb_line_no in source ] for i, (filename, _) in enumerate(stack): # Slice the stack to the first frame that matches @@ -176,6 +179,7 @@ class LogEntry: self, record: logging.LogRecord, paths_re: re.Pattern, + formatter: logging.Formatter | None = None, figure_out_source: bool = False, ) -> None: """Initialize a log entry.""" @@ -186,14 +190,21 @@ 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: str | None = None - extracted_tb: traceback.StackSummary | None = None + self.root_cause: tuple[str, int, str] | None = None + extracted_tb: list[tuple[FrameType, int]] | None = None if record.exc_info: - self.exception = "".join(traceback.format_exception(*record.exc_info)) - if extracted := traceback.extract_tb(record.exc_info[2]): + if formatter and record.exc_text is None: + record.exc_text = formatter.formatException(record.exc_info) + self.exception = record.exc_text or "" + if extracted := list(traceback.walk_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]) + tb_frame, tb_line_no = extracted[-1] + self.root_cause = ( + tb_frame.f_code.co_filename, + tb_line_no, + tb_frame.f_code.co_name, + ) if figure_out_source: self.source = _figure_out_source(record, paths_re, extracted_tb) else: @@ -273,7 +284,9 @@ class LogErrorHandler(logging.Handler): default upper limit is set to 50 (older entries are discarded) but can be changed if needed. """ - entry = LogEntry(record, self.paths_re, figure_out_source=True) + entry = LogEntry( + record, self.paths_re, formatter=self.formatter, figure_out_source=True + ) self.records.add_entry(entry) if self.fire_event: self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 5ab7a6f67b8..8f69ccdaffb 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -221,7 +221,7 @@ class TadoConnector: # Errors are planned to be converted to exceptions # in PyTado library, so this can be removed - if "errors" in mobile_devices and mobile_devices["errors"]: + if isinstance(mobile_devices, dict) and mobile_devices.get("errors"): _LOGGER.error( "Error for home ID %s while updating mobile devices: %s", self.home_id, @@ -256,7 +256,7 @@ class TadoConnector: # Errors are planned to be converted to exceptions # in PyTado library, so this can be removed - if "errors" in devices and devices["errors"]: + if isinstance(devices, dict) and devices.get("errors"): _LOGGER.error( "Error for home ID %s while updating devices: %s", self.home_id, diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 621f5a1ad61..6d298a80e79 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -206,7 +206,7 @@ def create_climate_entity( cool_max_temp = float(cool_temperatures["celsius"]["max"]) cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS) - entity = TadoClimate( + return TadoClimate( tado, name, zone_id, @@ -222,7 +222,6 @@ def create_climate_entity( cool_step, supported_fan_modes, ) - return entity class TadoClimate(TadoZoneEntity, ClimateEntity): diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 99172228973..f1257f097eb 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -106,7 +106,7 @@ def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zon min_temp = None max_temp = None - entity = TadoWaterHeater( + return TadoWaterHeater( tado, name, zone_id, @@ -115,8 +115,6 @@ def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zon max_temp, ) - return entity - class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Representation of a Tado water heater.""" diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index e0b3376ca2e..b4d972f7c06 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -126,7 +126,7 @@ class TankUtilitySensor(SensorEntity): self._token = auth.get_token(self._email, self._password, force=True) data = tank_monitor.get_device_data(self._token, self.device) else: - raise http_error + raise data.update(data.pop("device", {})) data.update(data.pop("lastReading", {})) return data diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 7443fa72b5b..ac009b7a274 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -21,7 +21,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = TankerkoenigDataUpdateCoordinator( hass, - entry, name=entry.unique_id or DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL, ) diff --git a/homeassistant/components/tankerkoenig/coordinator.py b/homeassistant/components/tankerkoenig/coordinator.py index 447099d2dca..458c629f422 100644 --- a/homeassistant/components/tankerkoenig/coordinator.py +++ b/homeassistant/components/tankerkoenig/coordinator.py @@ -17,9 +17,10 @@ from aiotankerkoenig import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP +from homeassistant.const import ATTR_ID, CONF_API_KEY, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -31,10 +32,11 @@ _LOGGER = logging.getLogger(__name__) class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): """Get the latest data from the API.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, name: str, update_interval: int, ) -> None: @@ -47,13 +49,14 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(minutes=update_interval), ) - self._selected_stations: list[str] = entry.data[CONF_STATIONS] + self._selected_stations: list[str] = self.config_entry.data[CONF_STATIONS] self.stations: dict[str, Station] = {} - self.fuel_types: list[str] = entry.data[CONF_FUEL_TYPES] - self.show_on_map: bool = entry.options[CONF_SHOW_ON_MAP] + self.fuel_types: list[str] = self.config_entry.data[CONF_FUEL_TYPES] + self.show_on_map: bool = self.config_entry.options[CONF_SHOW_ON_MAP] self._tankerkoenig = Tankerkoenig( - api_key=entry.data[CONF_API_KEY], session=async_get_clientsession(hass) + api_key=self.config_entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), ) async def async_setup(self) -> None: @@ -81,6 +84,27 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): self.stations[station_id] = station + entity_reg = er.async_get(self.hass) + for entity in er.async_entries_for_config_entry( + entity_reg, self.config_entry.entry_id + ): + if entity.unique_id.split("_")[0] not in self._selected_stations: + _LOGGER.debug("Removing obsolete entity entry %s", entity.entity_id) + entity_reg.async_remove(entity.entity_id) + + device_reg = dr.async_get(self.hass) + for device in dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ): + if not any( + (ATTR_ID, station_id) in device.identifiers + for station_id in self._selected_stations + ): + _LOGGER.debug("Removing obsolete device entry %s", device.name) + device_reg.async_update_device( + device.id, remove_config_entry_id=self.config_entry.entry_id + ) + if len(self.stations) > 10: _LOGGER.warning( "Found more than 10 stations to check. " diff --git a/homeassistant/components/tankerkoenig/diagnostics.py b/homeassistant/components/tankerkoenig/diagnostics.py index 4846d2687a2..0af5b29c5a8 100644 --- a/homeassistant/components/tankerkoenig/diagnostics.py +++ b/homeassistant/components/tankerkoenig/diagnostics.py @@ -27,11 +27,10 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - diag_data = { + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": { station_id: asdict(price_info) for station_id, price_info in coordinator.data.items() }, } - return diag_data diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index 4570d0e5649..c754094655d 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "iot_class": "cloud_polling", "loggers": ["aiotankerkoenig"], + "quality_scale": "platinum", "requirements": ["aiotankerkoenig==0.4.1"] } diff --git a/homeassistant/components/tasmota/device_automation.py b/homeassistant/components/tasmota/device_automation.py index ef05585dd87..af14efbd65c 100644 --- a/homeassistant/components/tasmota/device_automation.py +++ b/homeassistant/components/tasmota/device_automation.py @@ -1,7 +1,6 @@ """Provides device automations for Tasmota.""" -from collections.abc import Mapping -from typing import Any +from __future__ import annotations from hatasmota.const import AUTOMATION_TYPE_TRIGGER from hatasmota.models import DiscoveryHashType @@ -9,7 +8,10 @@ from hatasmota.trigger import TasmotaTrigger from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from homeassistant.helpers.device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import device_trigger @@ -25,12 +27,16 @@ async def async_remove_automations(hass: HomeAssistant, device_id: str) -> None: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up Tasmota device automation dynamically through discovery.""" - async def async_device_removed(event: Event) -> None: + async def async_device_removed( + event: Event[EventDeviceRegistryUpdatedData], + ) -> None: """Handle the removal of a device.""" await async_remove_automations(hass, event.data["device_id"]) @callback - def _async_device_removed_filter(event_data: Mapping[str, Any]) -> bool: + def _async_device_removed_filter( + event_data: EventDeviceRegistryUpdatedData, + ) -> bool: """Filter device registry events.""" return event_data["action"] == "remove" diff --git a/homeassistant/components/tcp/common.py b/homeassistant/components/tcp/common.py index 46520134bf6..d6a7fb28f11 100644 --- a/homeassistant/components/tcp/common.py +++ b/homeassistant/components/tcp/common.py @@ -149,7 +149,6 @@ class TcpEntity(Entity): if value_template is not None: try: self._state = value_template.render(parse_result=False, value=value) - return except TemplateError: _LOGGER.error( "Unable to render template of %r with value: %r", @@ -157,5 +156,6 @@ class TcpEntity(Entity): value, ) return + return self._state = value diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 6338996256b..f672ae1547f 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -122,6 +122,7 @@ EVENT_TELEGRAM_SENT = "telegram_sent" PARSER_HTML = "html" PARSER_MD = "markdown" PARSER_MD2 = "markdownv2" +PARSER_PLAIN_TEXT = "plain_text" DEFAULT_TRUSTED_NETWORKS = [ip_network("149.154.160.0/20"), ip_network("91.108.4.0/22")] @@ -524,6 +525,7 @@ class TelegramNotificationService: PARSER_HTML: ParseMode.HTML, PARSER_MD: ParseMode.MARKDOWN, PARSER_MD2: ParseMode.MARKDOWN_V2, + PARSER_PLAIN_TEXT: None, } self._parse_mode = self._parsers.get(parser) self.bot = bot @@ -687,11 +689,12 @@ class TelegramNotificationService: _LOGGER.warning( "Update last message: out_type:%s, out=%s", type(out), out ) - return out except TelegramError as exc: _LOGGER.error( "%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg ) + return None + return out async def send_message(self, message="", target=None, **kwargs): """Send a message to one or multiple pre-allowed chat IDs.""" @@ -982,10 +985,9 @@ class TelegramNotificationService: """Remove bot from chat.""" chat_id = self._get_target_chat_ids(chat_id)[0] _LOGGER.debug("Leave from chat ID %s", chat_id) - leaved = await self._send_msg( + return await self._send_msg( self.bot.leave_chat, "Error leaving chat", None, chat_id ) - return leaved class BaseTelegramBotEntity: diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 1587f754508..d2195c1d6ce 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -22,6 +22,7 @@ send_message: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -94,6 +95,7 @@ send_photo: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -229,6 +231,7 @@ send_animation: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -300,6 +303,7 @@ send_video: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -435,6 +439,7 @@ send_document: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -587,6 +592,7 @@ edit_message: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_web_page_preview: selector: boolean: diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 06ba656dd0d..7138f40a653 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -66,7 +66,7 @@ def reset_devices(): This assumes the same sensor devices are present in the same order. """ temper_devices = get_temper_devices() - for sensor, device in zip(TEMPER_SENSORS, temper_devices): + for sensor, device in zip(TEMPER_SENSORS, temper_devices, strict=False): sensor.set_temper_device(device) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 6d4d3a9367c..f881e61fb76 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -109,7 +109,8 @@ async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None: "entities": conf_section[platform_domain], }, hass_config, - ) + ), + eager_start=True, ) if coordinator_tasks: diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 3319afa01c2..d2ce44a0ad1 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -47,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, run_immediately=True + EVENT_HOMEASSISTANT_START, self._attach_triggers ) for platform_domain in PLATFORMS: @@ -59,7 +59,8 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): DOMAIN, {"coordinator": self, "entities": self.config[platform_domain]}, hass_config, - ) + ), + eager_start=True, ) async def _attach_triggers(self, start_event=None) -> None: diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a6dbedc6161..a341fdd5f87 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -237,8 +237,7 @@ def async_create_preview_sensor( ) -> SensorTemplate: """Create a preview sensor.""" validated_config = SENSOR_SCHEMA(config | {CONF_NAME: name}) - entity = SensorTemplate(hass, validated_config, None) - return entity + return SensorTemplate(hass, validated_config, None) class SensorTemplate(TemplateEntity, SensorEntity): diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 735fa7ddd23..a03b0a1ada0 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -4,9 +4,10 @@ from __future__ import annotations from collections.abc import Callable, Mapping import contextlib +from functools import cached_property import itertools import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -22,6 +23,7 @@ from homeassistant.core import ( CALLBACK_TYPE, Context, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -31,7 +33,6 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, TrackTemplateResultInfo, @@ -58,11 +59,6 @@ from .const import ( CONF_PICTURE, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) TEMPLATE_ENTITY_AVAILABILITY_SCHEMA = vol.Schema( diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 8e95362ff88..09ad0754634 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -8,10 +8,16 @@ 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, Event, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HassJob, + HomeAssistant, + callback, +) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, async_call_later, diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 632db28ca3a..c78c2bc2312 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -96,9 +96,7 @@ def get_model_detection_function(model): image, shapes = model.preprocess(image) prediction_dict = model.predict(image, shapes) - detections = model.postprocess(prediction_dict, shapes) - - return detections + return model.postprocess(prediction_dict, shapes) return detect_fn @@ -378,7 +376,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): matches = {} total_matches = 0 - for box, score, obj_class in zip(boxes, scores, classes): + for box, score, obj_class in zip(boxes, scores, classes, strict=False): score = score * 100 boxes = box.tolist() diff --git a/homeassistant/components/tesla_wall_connector/config_flow.py b/homeassistant/components/tesla_wall_connector/config_flow.py index b00dd8f2b9d..44848cb1dfe 100644 --- a/homeassistant/components/tesla_wall_connector/config_flow.py +++ b/homeassistant/components/tesla_wall_connector/config_flow.py @@ -104,8 +104,8 @@ class TeslaWallConnectorConfigFlow(ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, user_input) except WallConnectorError: errors["base"] = "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 1da3533fef1..45fd1eee327 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -4,6 +4,7 @@ import asyncio from typing import Final from tesla_fleet_api import EnergySpecific, Teslemetry, VehicleSpecific +from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import ( InvalidToken, SubscriptionRequired, @@ -13,10 +14,10 @@ from tesla_fleet_api.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, 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, LOGGER +from .const import DOMAIN from .coordinator import ( TeslemetryEnergyDataCoordinator, TeslemetryVehicleDataCoordinator, @@ -37,13 +38,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: access_token=access_token, ) try: + scopes = (await teslemetry.metadata())["scopes"] products = (await teslemetry.products())["response"] - except InvalidToken: - LOGGER.error("Access token is invalid, unable to connect to Teslemetry") - return False - except SubscriptionRequired: - LOGGER.error("Subscription required, unable to connect to Telemetry") - return False + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: raise ConfigEntryNotReady from e @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: vehicles: list[TeslemetryVehicleData] = [] energysites: list[TeslemetryEnergyData] = [] for product in products: - if "vin" in product: + if "vin" in product and Scope.VEHICLE_DEVICE_DATA in scopes: vin = product["vin"] api = VehicleSpecific(teslemetry.vehicle, vin) coordinator = TeslemetryVehicleDataCoordinator(hass, api) @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: vin=vin, ) ) - elif "energy_site_id" in product: + elif "energy_site_id" in product and Scope.ENERGY_DEVICE_DATA in scopes: site_id = product["energy_site_id"] api = EnergySpecific(teslemetry.energy, site_id) energysites.append( @@ -88,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setup Platforms hass.data.setdefault(DOMAIN, {})[entry.entry_id] = TeslemetryData( - vehicles, energysites + vehicles, energysites, scopes ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index 0835785d194..4c1c05570ab 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -4,6 +4,8 @@ from __future__ import annotations from typing import Any +from tesla_fleet_api.const import Scope + from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -17,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TeslemetryClimateSide from .context import handle_command from .entity import TeslemetryVehicleEntity +from .models import TeslemetryVehicleData async def async_setup_entry( @@ -26,7 +29,7 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER) + TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER, data.scopes) for vehicle in data.vehicles ) @@ -48,6 +51,22 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): _attr_preset_modes = ["off", "keep", "dog", "camp"] _enable_turn_on_off_backwards_compatibility = False + def __init__( + self, + data: TeslemetryVehicleData, + side: TeslemetryClimateSide, + scopes: Scope, + ) -> None: + """Initialize the climate.""" + self.scoped = Scope.VEHICLE_CMDS in scopes + if not self.scoped: + self._attr_supported_features = ClimateEntityFeature(0) + + super().__init__( + data, + side, + ) + @property def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" @@ -82,6 +101,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): async def async_turn_on(self) -> None: """Set the climate state to on.""" + self.raise_for_scope() with handle_command(): await self.wake_up_if_asleep() await self.api.auto_conditioning_start() @@ -89,6 +109,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): async def async_turn_off(self) -> None: """Set the climate state to off.""" + self.raise_for_scope() with handle_command(): await self.wake_up_if_asleep() await self.api.auto_conditioning_stop() diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index f7fc5bbf805..5fb6ce56aed 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -14,7 +14,7 @@ from tesla_fleet_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_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -27,37 +27,42 @@ DESCRIPTION_PLACEHOLDERS = { } -class TeslemetryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): """Config Teslemetry API connection.""" VERSION = 1 + _entry: ConfigEntry | None = None + + async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: + """Reusable Auth Helper.""" + teslemetry = Teslemetry( + session=async_get_clientsession(self.hass), + access_token=user_input[CONF_ACCESS_TOKEN], + ) + try: + await teslemetry.test() + except InvalidToken: + return {CONF_ACCESS_TOKEN: "invalid_access_token"} + except SubscriptionRequired: + return {"base": "subscription_required"} + except ClientConnectionError: + return {"base": "cannot_connect"} + except TeslaFleetError as e: + LOGGER.error(e) + return {"base": "unknown"} + return {} async def async_step_user( self, user_input: Mapping[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Get configuration from the user.""" errors: dict[str, str] = {} - if user_input: - teslemetry = Teslemetry( - session=async_get_clientsession(self.hass), - access_token=user_input[CONF_ACCESS_TOKEN], + if user_input and not (errors := await self.async_auth(user_input)): + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Teslemetry", + data=user_input, ) - try: - await teslemetry.test() - except InvalidToken: - errors[CONF_ACCESS_TOKEN] = "invalid_access_token" - except SubscriptionRequired: - errors["base"] = "subscription_required" - except ClientConnectionError: - errors["base"] = "cannot_connect" - except TeslaFleetError as e: - LOGGER.exception(str(e)) - errors["base"] = "unknown" - else: - return self.async_create_entry( - title="Teslemetry", - data=user_input, - ) return self.async_show_form( step_id="user", @@ -65,3 +70,31 @@ class TeslemetryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=DESCRIPTION_PLACEHOLDERS, errors=errors, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth on failure.""" + 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: Mapping[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle users reauth credentials.""" + + assert self._entry + errors: dict[str, str] = {} + + if user_input and not (errors := await self.async_auth(user_input)): + return self.async_update_reload_and_abort( + self._entry, + data=user_input, + ) + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders=DESCRIPTION_PLACEHOLDERS, + data_schema=TESLEMETRY_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 27ff45f75a3..be34386a508 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -5,10 +5,15 @@ from typing import Any from tesla_fleet_api import EnergySpecific, VehicleSpecific from tesla_fleet_api.const import VehicleDataEndpoint -from tesla_fleet_api.exceptions import TeslaFleetError, VehicleOffline +from tesla_fleet_api.exceptions import ( + InvalidToken, + SubscriptionRequired, + TeslaFleetError, + VehicleOffline, +) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER, TeslemetryState @@ -54,6 +59,10 @@ class TeslemetryVehicleDataCoordinator(TeslemetryDataCoordinator): if response["response"]["state"] != TeslemetryState.ONLINE: # The first refresh will fail, so retry later raise ConfigEntryNotReady("Vehicle is not online") + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: # The first refresh will also fail, so retry later raise ConfigEntryNotReady from e @@ -67,6 +76,10 @@ class TeslemetryVehicleDataCoordinator(TeslemetryDataCoordinator): except VehicleOffline: self.data["state"] = TeslemetryState.OFFLINE return self.data + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: raise UpdateFailed(e.message) from e @@ -97,12 +110,16 @@ class TeslemetryEnergyDataCoordinator(TeslemetryDataCoordinator): try: data = await self.api.live_status() + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e 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", []) + wc["din"]: wc for wc in (data["response"].get("wall_connectors") or []) } return data["response"] diff --git a/homeassistant/components/teslemetry/diagnostics.py b/homeassistant/components/teslemetry/diagnostics.py new file mode 100644 index 00000000000..f8a8e6727a7 --- /dev/null +++ b/homeassistant/components/teslemetry/diagnostics.py @@ -0,0 +1,46 @@ +"""Provides diagnostics for Teslemetry.""" + +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 + +VEHICLE_REDACT = [ + "id", + "user_id", + "vehicle_id", + "vin", + "tokens", + "id_s", + "drive_state_active_route_latitude", + "drive_state_active_route_longitude", + "drive_state_latitude", + "drive_state_longitude", + "drive_state_native_latitude", + "drive_state_native_longitude", +] + +ENERGY_REDACT = ["vin"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + vehicles = [ + x.coordinator.data for x in hass.data[DOMAIN][config_entry.entry_id].vehicles + ] + energysites = [ + x.coordinator.data for x in hass.data[DOMAIN][config_entry.entry_id].energysites + ] + + # Return only the relevant children + return { + "vehicles": async_redact_data(vehicles, VEHICLE_REDACT), + "energysites": async_redact_data(energysites, ENERGY_REDACT), + } diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index eda3d26f341..d67a1bd1770 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -5,7 +5,7 @@ from typing import Any from tesla_fleet_api.exceptions import TeslaFleetError -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -83,6 +83,11 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator self.coordinator.data[key] = value self.async_write_ha_state() + def raise_for_scope(self): + """Raise an error if a scope is not available.""" + if not self.scoped: + raise ServiceValidationError("Missing required scope") + class TeslemetryEnergyEntity(CoordinatorEntity[TeslemetryEnergyDataCoordinator]): """Parent class for Teslemetry Energy Entities.""" diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index d6f15e2e932..615156e6fdc 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -6,6 +6,7 @@ import asyncio from dataclasses import dataclass from tesla_fleet_api import EnergySpecific, VehicleSpecific +from tesla_fleet_api.const import Scope from .coordinator import ( TeslemetryEnergyDataCoordinator, @@ -19,6 +20,7 @@ class TeslemetryData: vehicles: list[TeslemetryVehicleData] energysites: list[TeslemetryEnergyData] + scopes: list[Scope] @dataclass diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 19d2d2c4869..bea1bf72a8d 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -66,7 +66,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): if e.status == HTTPStatus.UNAUTHORIZED: # Auth Token is no longer valid raise ConfigEntryAuthFailed from e - raise e + raise return self._flatten(vehicle) diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 09402055ee8..1e5653744fb 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -12,10 +12,14 @@ from tessie_api import ( unlock, ) +from homeassistant.components.automation import automations_with_entity from homeassistant.components.lock import ATTR_CODE, LockEntity +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.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TessieChargeCableLockStates @@ -29,11 +33,46 @@ async def async_setup_entry( """Set up the Tessie sensor platform from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ klass(vehicle.state_coordinator) - for klass in (TessieLockEntity, TessieCableLockEntity, TessieSpeedLimitEntity) + for klass in (TessieLockEntity, TessieCableLockEntity) for vehicle in data - ) + ] + + ent_reg = er.async_get(hass) + + for vehicle in data: + entity_id = ent_reg.async_get_entity_id( + Platform.LOCK, + DOMAIN, + f"{vehicle.state_coordinator.vin}-vehicle_state_speed_limit_mode_active", + ) + if entity_id: + entity_entry = ent_reg.async_get(entity_id) + assert entity_entry + if entity_entry.disabled: + ent_reg.async_remove(entity_id) + else: + entities.append(TessieSpeedLimitEntity(vehicle.state_coordinator)) + + entity_automations = automations_with_entity(hass, entity_id) + entity_scripts = scripts_with_entity(hass, entity_id) + for item in entity_automations + entity_scripts: + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_speed_limit_{entity_id}_{item}", + breaks_in_ha_version="2024.11.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_speed_limit_entity", + translation_placeholders={ + "entity": entity_id, + "info": item, + }, + ) + async_add_entities(entities) class TessieLockEntity(TessieEntity, LockEntity): @@ -81,6 +120,16 @@ class TessieSpeedLimitEntity(TessieEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Enable speed limit with pin.""" + ir.async_create_issue( + self.coordinator.hass, + DOMAIN, + "deprecated_speed_limit_locked", + breaks_in_ha_version="2024.11.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_speed_limit_locked", + ) code: str | None = kwargs.get(ATTR_CODE) if code: await self.run(enable_speed_limit, pin=code) @@ -88,6 +137,16 @@ class TessieSpeedLimitEntity(TessieEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Disable speed limit with pin.""" + ir.async_create_issue( + self.coordinator.hass, + DOMAIN, + "deprecated_speed_limit_unlocked", + breaks_in_ha_version="2024.11.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_speed_limit_unlocked", + ) code: str | None = kwargs.get(ATTR_CODE) if code: await self.run(disable_speed_limit, pin=code) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 8e1e47f934f..ea75660ddb7 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -410,5 +410,40 @@ "no_cable": { "message": "Insert cable to lock" } + }, + "issues": { + "deprecated_speed_limit_entity": { + "title": "Detected Tessie speed limit lock entity usage", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tessie::issues::deprecated_speed_limit_entity::title%]", + "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\nHome Assistant detected that entity `{entity}` is being used in `{info}`\n\nYou should remove the speed limit lock entity from `{info}` then click submit to fix this issue." + } + } + } + }, + "deprecated_speed_limit_locked": { + "title": "Detected Tessie speed limit lock entity locked", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tessie::issues::deprecated_speed_limit_locked::title%]", + "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\n\nPlease remove this entity from any automation or script, disable the entity then click submit to fix this issue." + } + } + } + }, + "deprecated_speed_limit_unlocked": { + "title": "Detected Tessie speed limit lock entity unlocked", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tessie::issues::deprecated_speed_limit_unlocked::title%]", + "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\n\nPlease remove this entity from any automation or script, disable the entity then click submit to fix this issue." + } + } + } + } } } diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py index cf29910cc34..f45a9cf3563 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -5,9 +5,10 @@ from __future__ import annotations from dataclasses import asdict, dataclass from datetime import timedelta from enum import StrEnum +from functools import cached_property import logging import re -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -34,11 +35,6 @@ from .const import ( SERVICE_SET_VALUE, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/thermobeacon/strings.json b/homeassistant/components/thermobeacon/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/thermobeacon/strings.json +++ b/homeassistant/components/thermobeacon/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/thermopro/strings.json b/homeassistant/components/thermopro/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/thermopro/strings.json +++ b/homeassistant/components/thermopro/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index de322510ef2..b880be801a4 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -5,13 +5,13 @@ from __future__ import annotations from asyncio import Event, Task, wait import dataclasses from datetime import datetime +from functools import cached_property import logging from typing import Any, cast from python_otbr_api import tlv_parser from python_otbr_api.tlv_parser import MeshcopTLVType -from homeassistant.backports.functools import cached_property from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.singleton import singleton diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 364511ca291..9674357eb60 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -22,7 +22,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -30,10 +30,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index da2fd881a54..7da0a2b7947 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -53,6 +53,8 @@ from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER +FIVE_YEARS = 5 * 365 * 24 + _LOGGER = logging.getLogger(__name__) ICON = "mdi:currency-usd" @@ -724,9 +726,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]): # pylint: disable=has None, {"sum"}, ) - first_stat = stat[statistic_id][0] - _sum = cast(float, first_stat["sum"]) - last_stats_time = first_stat["start"] + if statistic_id in stat: + first_stat = stat[statistic_id][0] + _sum = cast(float, first_stat["sum"]) + last_stats_time = first_stat["start"] + else: + hourly_data = await home.get_historic_data( + FIVE_YEARS, production=is_production + ) + _sum = 0.0 + last_stats_time = None statistics = [] diff --git a/homeassistant/components/tilt_ble/strings.json b/homeassistant/components/tilt_ble/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/tilt_ble/strings.json +++ b/homeassistant/components/tilt_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/time/__init__.py b/homeassistant/components/time/__init__.py index 2e87aaac28d..4e101ddd67d 100644 --- a/homeassistant/components/time/__init__.py +++ b/homeassistant/components/time/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import time, timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -22,12 +23,6 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 74ee99b811f..e574c6372a7 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -3,8 +3,9 @@ from collections.abc import Callable, Iterable import dataclasses import datetime +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -41,12 +42,6 @@ from .const import ( TodoListEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(seconds=60) diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 371121a9da3..f3ca5302b2a 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -100,7 +100,7 @@ class TomorrowioSensorEntityDescription(SensorEntityDescription): # From https://cfpub.epa.gov/ncer_abstracts/index.cfm/fuseaction/display.files/fileID/14285 # x ug/m^3 = y ppb * molecular weight / 24.45 -def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], float]: +def convert_ppb_to_ugm3(molecular_weight: float) -> Callable[[float], float]: """Return function to convert ppb to ug/m^3.""" return lambda x: (x * molecular_weight) / 24.45 @@ -339,7 +339,7 @@ async def async_setup_entry( def handle_conversion( - value: float | int, conversion: Callable[[float], float] | float + value: float, conversion: Callable[[float], float] | float ) -> float: """Handle conversion of a value based on conversion type.""" if callable(conversion): diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 8edf4fe49fc..8572a5a0bba 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -7,10 +7,10 @@ import re from aiohttp import web import voluptuous as vol -from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE -from homeassistant.core import HassJob, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,10 +44,10 @@ def convert_pid(value): return int(value, 16) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Torque platform.""" @@ -56,7 +56,7 @@ def setup_platform( sensors: dict[int, TorqueSensor] = {} hass.http.register_view( - TorqueReceiveDataView(email, vehicle, sensors, add_entities) + TorqueReceiveDataView(email, vehicle, sensors, async_add_entities) ) @@ -71,18 +71,17 @@ class TorqueReceiveDataView(HomeAssistantView): email: str | None, vehicle: str | None, sensors: dict[int, TorqueSensor], - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Initialize a Torque view.""" self.email = email self.vehicle = vehicle self.sensors = sensors - self.add_entities_job = HassJob(add_entities) + self.async_add_entities = async_add_entities @callback def get(self, request: web.Request) -> str | None: """Handle Torque data request.""" - hass: HomeAssistant = request.app[KEY_HASS] data = request.query if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]: @@ -111,12 +110,17 @@ class TorqueReceiveDataView(HomeAssistantView): if pid in self.sensors: self.sensors[pid].async_on_update(data[key]) + new_sensor_entities: list[TorqueSensor] = [] for pid, name in names.items(): if pid not in self.sensors: - self.sensors[pid] = TorqueSensor( + torque_sensor_entity = TorqueSensor( ENTITY_NAME_FORMAT.format(self.vehicle, name), units.get(pid) ) - hass.async_add_hass_job(self.add_entities_job, [self.sensors[pid]]) + new_sensor_entities.append(torque_sensor_entity) + self.sensors[pid] = torque_sensor_entity + + if new_sensor_entities: + self.async_add_entities(new_sensor_entities) return "OK!" diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 3f2c51989f9..1de9db1d319 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -4,9 +4,12 @@ from __future__ import annotations from total_connect_client import ArmingHelper from total_connect_client.exceptions import BadResultCodeError, UsercodeInvalid +from total_connect_client.location import TotalConnectLocation -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, @@ -21,12 +24,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .entity import TotalConnectLocationEntity SERVICE_ALARM_ARM_AWAY_INSTANT = "arm_away_instant" SERVICE_ALARM_ARM_HOME_INSTANT = "arm_home_instant" @@ -36,23 +38,17 @@ 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: list[TotalConnectAlarm] = [] - coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - for location_id, location in coordinator.client.locations.items(): - location_name = location.location_name - 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( + TotalConnectAlarm( + coordinator, + location, + partition_id, ) - - async_add_entities(alarms) + for location in coordinator.client.locations.values() + for partition_id in location.partitions + ) # Set up services platform = entity_platform.async_get_current_platform() @@ -70,10 +66,8 @@ async def async_setup_entry( ) -class TotalConnectAlarm( - CoordinatorEntity[TotalConnectDataUpdateCoordinator], alarm.AlarmControlPanelEntity -): - """Represent an TotalConnect status.""" +class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): + """Represent a TotalConnect alarm panel.""" _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME @@ -84,19 +78,13 @@ class TotalConnectAlarm( def __init__( self, coordinator: TotalConnectDataUpdateCoordinator, - name, - location_id, - partition_id, + location: TotalConnectLocation, + partition_id: int, ) -> None: """Initialize the TotalConnect status.""" - super().__init__(coordinator) - self._location_id = location_id - self._location = coordinator.client.locations[location_id] + super().__init__(coordinator, location) self._partition_id = partition_id self._partition = self._location.partitions[partition_id] - self._device = self._location.devices[self._location.security_device_id] - self._state: str | None = None - self._attr_extra_state_attributes = {} """ Set unique_id to location_id for partition 1 to avoid breaking change @@ -104,26 +92,18 @@ class TotalConnectAlarm( Add _# for partition 2 and beyond. """ if partition_id == 1: - self._attr_name = name - self._attr_unique_id = f"{location_id}" + self._attr_name = None + self._attr_unique_id = str(location.location_id) else: - self._attr_name = f"{name} partition {partition_id}" - self._attr_unique_id = f"{location_id}_{partition_id}" - - @property - def device_info(self) -> DeviceInfo: - """Return device info.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device.serial_number)}, - name=self._device.name, - ) + self._attr_translation_key = "partition" + self._attr_translation_placeholders = {"partition_id": str(partition_id)} + self._attr_unique_id = f"{location.location_id}_{partition_id}" @property def state(self) -> str | None: """Return the state of the device.""" attr = { - "location_name": self.name, - "location_id": self._location_id, + "location_id": self._location.location_id, "partition": self._partition_id, "ac_loss": self._location.ac_loss, "low_battery": self._location.low_battery, @@ -132,6 +112,11 @@ class TotalConnectAlarm( "triggered_zone": None, } + if self._partition_id == 1: + attr["location_name"] = self.device.name + else: + attr["location_name"] = f"{self.device.name} partition {self._partition_id}" + state: str | None = None if self._partition.arming_state.is_disarmed(): state = STATE_ALARM_DISARMED @@ -157,10 +142,9 @@ class TotalConnectAlarm( state = STATE_ALARM_TRIGGERED attr["triggered_source"] = "Carbon Monoxide" - self._state = state self._attr_extra_state_attributes = attr - return self._state + return state async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -173,7 +157,7 @@ class TotalConnectAlarm( ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to disarm {self.name}." + f"TotalConnect failed to disarm {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -192,7 +176,7 @@ class TotalConnectAlarm( ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm home {self.name}." + f"TotalConnect failed to arm home {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -211,7 +195,7 @@ class TotalConnectAlarm( ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm away {self.name}." + f"TotalConnect failed to arm away {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -230,7 +214,7 @@ class TotalConnectAlarm( ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm night {self.name}." + f"TotalConnect failed to arm night {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -249,7 +233,7 @@ class TotalConnectAlarm( ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm home instant {self.name}." + f"TotalConnect failed to arm home instant {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -268,7 +252,7 @@ class TotalConnectAlarm( ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm away instant {self.name}." + f"TotalConnect failed to arm away instant {self.device.name}." ) from error await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index c6c7c75e0b5..85461805124 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -1,7 +1,12 @@ """Interfaces with TotalConnect sensors.""" +from collections.abc import Callable +from dataclasses import dataclass import logging +from total_connect_client.location import TotalConnectLocation +from total_connect_client.zone import TotalConnectZone + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -12,7 +17,9 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .entity import TotalConnectLocationEntity, TotalConnectZoneEntity LOW_BATTERY = "low_battery" TAMPER = "tamper" @@ -22,162 +29,172 @@ ZONE = "zone" _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class TotalConnectZoneBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes TotalConnect binary sensor entity.""" + + device_class_fn: Callable[[TotalConnectZone], BinarySensorDeviceClass] | None = None + is_on_fn: Callable[[TotalConnectZone], bool] + + +def get_security_zone_device_class(zone: TotalConnectZone) -> BinarySensorDeviceClass: + """Return the device class of a TotalConnect security zone.""" + if zone.is_type_fire(): + return BinarySensorDeviceClass.SMOKE + if zone.is_type_carbon_monoxide(): + return BinarySensorDeviceClass.GAS + if zone.is_type_motion(): + return BinarySensorDeviceClass.MOTION + if zone.is_type_medical(): + return BinarySensorDeviceClass.SAFETY + if zone.is_type_temperature(): + return BinarySensorDeviceClass.PROBLEM + return BinarySensorDeviceClass.DOOR + + +SECURITY_BINARY_SENSOR = TotalConnectZoneBinarySensorEntityDescription( + key=ZONE, + name=None, + device_class_fn=get_security_zone_device_class, + is_on_fn=lambda zone: zone.is_faulted() or zone.is_triggered(), +) + +NO_BUTTON_BINARY_SENSORS: tuple[TotalConnectZoneBinarySensorEntityDescription, ...] = ( + TotalConnectZoneBinarySensorEntityDescription( + key=LOW_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda zone: zone.is_low_battery(), + ), + TotalConnectZoneBinarySensorEntityDescription( + key=TAMPER, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda zone: zone.is_tampered(), + ), +) + + +@dataclass(frozen=True, kw_only=True) +class TotalConnectAlarmBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes TotalConnect binary sensor entity.""" + + is_on_fn: Callable[[TotalConnectLocation], bool] + + +LOCATION_BINARY_SENSORS: tuple[TotalConnectAlarmBinarySensorEntityDescription, ...] = ( + TotalConnectAlarmBinarySensorEntityDescription( + key=LOW_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda location: location.is_low_battery(), + ), + TotalConnectAlarmBinarySensorEntityDescription( + key=TAMPER, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda location: location.is_cover_tampered(), + ), + TotalConnectAlarmBinarySensorEntityDescription( + key=POWER, + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda location: location.is_ac_loss(), + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up TotalConnect device sensors based on a config entry.""" sensors: list = [] - client_locations = hass.data[DOMAIN][entry.entry_id].client.locations + coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + client_locations = coordinator.client.locations for location_id, location in client_locations.items(): - sensors.append(TotalConnectAlarmLowBatteryBinarySensor(location)) - sensors.append(TotalConnectAlarmTamperBinarySensor(location)) - sensors.append(TotalConnectAlarmPowerBinarySensor(location)) + sensors.extend( + TotalConnectAlarmBinarySensor(coordinator, description, location) + for description in LOCATION_BINARY_SENSORS + ) for zone in location.zones.values(): - sensors.append(TotalConnectZoneSecurityBinarySensor(location_id, zone)) + sensors.append( + TotalConnectZoneBinarySensor( + coordinator, SECURITY_BINARY_SENSOR, zone, location_id + ) + ) if not zone.is_type_button(): - sensors.append(TotalConnectLowBatteryBinarySensor(location_id, zone)) - sensors.append(TotalConnectTamperBinarySensor(location_id, zone)) + sensors.extend( + TotalConnectZoneBinarySensor( + coordinator, + description, + zone, + location_id, + ) + for description in NO_BUTTON_BINARY_SENSORS + ) - async_add_entities(sensors, True) + async_add_entities(sensors) -class TotalConnectZoneBinarySensor(BinarySensorEntity): - """Represent an TotalConnect zone.""" +class TotalConnectZoneBinarySensor(TotalConnectZoneEntity, BinarySensorEntity): + """Represent a TotalConnect zone.""" - def __init__(self, location_id, zone): + entity_description: TotalConnectZoneBinarySensorEntityDescription + + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + entity_description: TotalConnectZoneBinarySensorEntityDescription, + zone: TotalConnectZone, + location_id: str, + ) -> None: """Initialize the TotalConnect status.""" - self._location_id = location_id - self._zone = zone - self._attr_name = f"{zone.description}{self.entity_description.name}" - self._attr_unique_id = ( - f"{location_id}_{zone.zoneid}_{self.entity_description.key}" - ) - self._attr_is_on = None + super().__init__(coordinator, zone, location_id, entity_description.key) + self.entity_description = entity_description self._attr_extra_state_attributes = { - "zone_id": self._zone.zoneid, - "location_id": self._location_id, - "partition": self._zone.partition, + "zone_id": zone.zoneid, + "location_id": location_id, + "partition": zone.partition, } - -class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor): - """Represent an TotalConnect security zone.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=ZONE, name="" - ) - @property - def device_class(self): + def is_on(self) -> bool: + """Return the state of the entity.""" + return self.entity_description.is_on_fn(self._zone) + + @property + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this zone.""" - if self._zone.is_type_fire(): - return BinarySensorDeviceClass.SMOKE - if self._zone.is_type_carbon_monoxide(): - return BinarySensorDeviceClass.GAS - if self._zone.is_type_motion(): - return BinarySensorDeviceClass.MOTION - if self._zone.is_type_medical(): - return BinarySensorDeviceClass.SAFETY - if self._zone.is_type_temperature(): - return BinarySensorDeviceClass.PROBLEM - return BinarySensorDeviceClass.DOOR - - def update(self): - """Return the state of the device.""" - if self._zone.is_faulted() or self._zone.is_triggered(): - self._attr_is_on = True - else: - self._attr_is_on = False + if self.entity_description.device_class_fn: + return self.entity_description.device_class_fn(self._zone) + return super().device_class -class TotalConnectLowBatteryBinarySensor(TotalConnectZoneBinarySensor): - """Represent an TotalConnect zone low battery status.""" +class TotalConnectAlarmBinarySensor(TotalConnectLocationEntity, BinarySensorEntity): + """Represent a TotalConnect alarm device binary sensors.""" - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=LOW_BATTERY, - device_class=BinarySensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - name=" low battery", - ) + entity_description: TotalConnectAlarmBinarySensorEntityDescription - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._zone.is_low_battery() - - -class TotalConnectTamperBinarySensor(TotalConnectZoneBinarySensor): - """Represent an TotalConnect zone tamper status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=TAMPER, - device_class=BinarySensorDeviceClass.TAMPER, - entity_category=EntityCategory.DIAGNOSTIC, - name=f" {TAMPER}", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._zone.is_tampered() - - -class TotalConnectAlarmBinarySensor(BinarySensorEntity): - """Represent an TotalConnect alarm device binary sensors.""" - - def __init__(self, location): + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + entity_description: TotalConnectAlarmBinarySensorEntityDescription, + location: TotalConnectLocation, + ) -> None: """Initialize the TotalConnect alarm device binary sensor.""" - self._location = location - self._attr_name = f"{location.location_name}{self.entity_description.name}" - self._attr_unique_id = f"{location.location_id}_{self.entity_description.key}" - self._attr_is_on = None + super().__init__(coordinator, location) + self.entity_description = entity_description + self._attr_unique_id = f"{location.location_id}_{entity_description.key}" self._attr_extra_state_attributes = { - "location_id": self._location.location_id, + "location_id": location.location_id, } - -class TotalConnectAlarmLowBatteryBinarySensor(TotalConnectAlarmBinarySensor): - """Represent an TotalConnect Alarm low battery status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=LOW_BATTERY, - device_class=BinarySensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - name=" low battery", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._location.is_low_battery() - - -class TotalConnectAlarmTamperBinarySensor(TotalConnectAlarmBinarySensor): - """Represent an TotalConnect alarm tamper status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=TAMPER, - device_class=BinarySensorDeviceClass.TAMPER, - entity_category=EntityCategory.DIAGNOSTIC, - name=f" {TAMPER}", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._location.is_cover_tampered() - - -class TotalConnectAlarmPowerBinarySensor(TotalConnectAlarmBinarySensor): - """Represent an TotalConnect alarm power status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=POWER, - device_class=BinarySensorDeviceClass.POWER, - entity_category=EntityCategory.DIAGNOSTIC, - name=f" {POWER}", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = not self._location.is_ac_loss() + @property + def is_on(self) -> bool: + """Return the state of the entity.""" + return self.entity_description.is_on_fn(self._location) diff --git a/homeassistant/components/totalconnect/entity.py b/homeassistant/components/totalconnect/entity.py new file mode 100644 index 00000000000..a18ffc14df5 --- /dev/null +++ b/homeassistant/components/totalconnect/entity.py @@ -0,0 +1,57 @@ +"""Base class for TotalConnect entities.""" + +from total_connect_client.location import TotalConnectLocation +from total_connect_client.zone import TotalConnectZone + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import DOMAIN, TotalConnectDataUpdateCoordinator + + +class TotalConnectEntity(CoordinatorEntity[TotalConnectDataUpdateCoordinator]): + """Represent a TotalConnect entity.""" + + _attr_has_entity_name = True + + +class TotalConnectLocationEntity(TotalConnectEntity): + """Represent a TotalConnect location.""" + + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + location: TotalConnectLocation, + ) -> None: + """Initialize the TotalConnect location.""" + super().__init__(coordinator) + self._location = location + self.device = device = location.devices[location.security_device_id] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.serial_number)}, + name=device.name, + serial_number=device.serial_number, + ) + + +class TotalConnectZoneEntity(TotalConnectEntity): + """Represent a TotalConnect zone.""" + + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + zone: TotalConnectZone, + location_id: str, + key: str, + ) -> None: + """Initialize the TotalConnect zone.""" + super().__init__(coordinator) + self._location_id = location_id + self._zone = zone + self._attr_unique_id = f"{location_id}_{zone.zoneid}_{key}" + identifier = zone.sensor_serial_number or f"zone_{zone.zoneid}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, identifier)}, + name=zone.description, + serial_number=zone.sensor_serial_number, + ) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 183919f05f2..d1afb01210d 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/totalconnect", "iot_class": "cloud_polling", "loggers": ["total_connect_client"], - "requirements": ["total-connect-client==2023.2"] + "requirements": ["total-connect-client==2023.12.1"] } diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 922962c9866..03656b60084 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -49,5 +49,12 @@ "name": "Arm home instant", "description": "Arms Home with zero entry delay." } + }, + "entity": { + "alarm_control_panel": { + "partition": { + "name": "Partition {partition_id}" + } + } } } diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 919e1a537e5..df3291561fa 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -174,7 +174,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): else: self._discovered_device = device await set_credentials(self.hass, username, password) - self.hass.async_create_task(self._async_reload_requires_auth_entries()) + self.hass.async_create_task( + self._async_reload_requires_auth_entries(), eager_start=False + ) return self._async_create_entry_from_device(self._discovered_device) self.context["title_placeholders"] = placeholders @@ -267,7 +269,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): placeholders["error"] = str(ex) else: await set_credentials(self.hass, username, password) - self.hass.async_create_task(self._async_reload_requires_auth_entries()) + self.hass.async_create_task( + self._async_reload_requires_auth_entries(), eager_start=False + ) return self._async_create_entry_from_device(device) return self.async_show_form( @@ -446,7 +450,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): placeholders["error"] = str(ex) else: await set_credentials(self.hass, username, password) - self.hass.async_create_task(self._async_reload_requires_auth_entries()) + self.hass.async_create_task( + self._async_reload_requires_auth_entries(), eager_start=False + ) return self.async_abort(reason="reauth_successful") # Old config entries will not have these values. diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 94ad94de0ae..7595cdd8f90 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -7,6 +7,7 @@ import logging from kasa import AuthenticationException, SmartDevice, SmartDeviceException +from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer @@ -20,6 +21,8 @@ REQUEST_REFRESH_DELAY = 0.35 class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): """DataUpdateCoordinator to gather data for a specific TPLink device.""" + config_entry: config_entries.ConfigEntry + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4720fae1259..23766e69257 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -5,8 +5,14 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from typing import Any, Concatenate, ParamSpec, TypeVar -from kasa import SmartDevice +from kasa import ( + AuthenticationException, + SmartDevice, + SmartDeviceException, + TimeoutException, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -21,10 +27,39 @@ _P = ParamSpec("_P") def async_refresh_after( func: Callable[Concatenate[_T, _P], Awaitable[None]], ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: - """Define a wrapper to refresh after.""" + """Define a wrapper to raise HA errors and refresh after.""" async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: - await func(self, *args, **kwargs) + try: + await func(self, *args, **kwargs) + except AuthenticationException as ex: + self.coordinator.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_authentication", + translation_placeholders={ + "func": func.__name__, + "exc": str(ex), + }, + ) from ex + except TimeoutException as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_timeout", + translation_placeholders={ + "func": func.__name__, + "exc": str(ex), + }, + ) from ex + except SmartDeviceException as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_error", + translation_placeholders={ + "func": func.__name__, + "exc": str(ex), + }, + ) from ex await self.coordinator.async_request_refresh() return _async_wrap diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index d007868930a..aa50c3f2ed2 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -228,7 +228,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): await self.device.set_hsv(hue, sat, brightness, transition=transition) async def _async_set_color_temp( - self, color_temp: float | int, brightness: int | None, transition: int | None + self, color_temp: float, brightness: int | None, transition: int | None ) -> None: device = self.device valid_temperature_range = device.valid_temperature_range diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 1f6b07365b5..d7563dd0401 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfPower, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import legacy_device_id @@ -171,8 +171,17 @@ class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): else: assert description.device_class self._attr_translation_key = f"{description.device_class.value}_child" + self._async_update_attrs() - @property - def native_value(self) -> float | None: - """Return the sensors state.""" - return async_emeter_from_device(self.device, self.entity_description) + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_native_value = async_emeter_from_device( + self.device, self.entity_description + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index 19aa35f3604..c863df7c81c 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -184,5 +184,16 @@ } } } + }, + "exceptions": { + "device_timeout": { + "message": "Timeout communicating with the device {func}: {exc}" + }, + "device_error": { + "message": "Unable to communicate with the device {func}: {exc}" + }, + "device_authentication": { + "message": "Device authentication error {func}: {exc}" + } } } diff --git a/homeassistant/components/tplink_omada/switch.py b/homeassistant/components/tplink_omada/switch.py index b8abb4cb773..9f9eeceb866 100644 --- a/homeassistant/components/tplink_omada/switch.py +++ b/homeassistant/components/tplink_omada/switch.py @@ -64,6 +64,7 @@ async def async_setup_entry( ]( coordinator, switch, + port, port.port_id, desc, port_name=_get_switch_port_base_name(port), @@ -79,7 +80,7 @@ async def async_setup_entry( entities.extend( OmadaDevicePortSwitchEntity[ OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortStatus - ](gateway_coordinator, gateway, p.port_number, desc) + ](gateway_coordinator, gateway, p, str(p.port_number), desc) for p in gateway.port_status for desc in GATEWAY_PORT_STATUS_SWITCHES if desc.exists_func(gateway, p) @@ -87,7 +88,7 @@ async def async_setup_entry( entities.extend( OmadaDevicePortSwitchEntity[ OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortConfig - ](gateway_coordinator, gateway, p.port_number, desc) + ](gateway_coordinator, gateway, p, str(p.port_number), desc) for p in gateway.port_configs for desc in GATEWAY_PORT_CONFIG_SWITCHES if desc.exists_func(gateway, p) @@ -111,12 +112,9 @@ class OmadaDevicePortSwitchEntityDescription( """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]] + coordinator_update_func: Callable[[TCoordinator, TDevice, TPort], TPort | None] + set_func: Callable[[OmadaSiteClient, TDevice, TPort, bool], Awaitable[TPort | None]] update_func: Callable[[TPort], bool] - refresh_after_set: bool = False @dataclass(frozen=True, kw_only=True) @@ -128,9 +126,9 @@ class OmadaSwitchPortSwitchEntityDescription( """Entity description for a toggle switch for a feature of a Port on an Omada Switch.""" coordinator_update_func: Callable[ - [OmadaSwitchPortCoordinator, OmadaSwitch, int | str], + [OmadaSwitchPortCoordinator, OmadaSwitch, OmadaSwitchPortDetails], OmadaSwitchPortDetails | None, - ] = lambda coord, _, port_id: coord.data.get(str(port_id)) + ] = lambda coord, _, port: coord.data.get(port.port_id) @dataclass(frozen=True, kw_only=True) @@ -142,10 +140,12 @@ class OmadaGatewayPortConfigSwitchEntityDescription( """Entity description for a toggle switch for a configuration of a Port on an Omada Gateway.""" coordinator_update_func: Callable[ - [OmadaGatewayCoordinator, OmadaGateway, int | str], + [OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortConfig], OmadaGatewayPortConfig | None, - ] = lambda coord, device, port_id: next( - p for p in coord.data[device.mac].port_configs if p.port_number == port_id + ] = lambda coord, device, port: next( + p + for p in coord.data[device.mac].port_configs + if p.port_number == port.port_number ) @@ -158,20 +158,24 @@ class OmadaGatewayPortStatusSwitchEntityDescription( """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 + [OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortStatus], + OmadaGatewayPortStatus, + ] = lambda coord, device, port: next( + p + for p in coord.data[device.mac].port_status + if p.port_number == port.port_number ) -def _wan_connect_disconnect( +async def _wan_connect_disconnect( client: OmadaSiteClient, device: OmadaDevice, port: OmadaGatewayPortStatus, enable: bool, ipv6: bool, -) -> Awaitable[OmadaGatewayPortStatus]: - return client.set_gateway_wan_port_connect_state( +) -> None: + # The state returned by the API is not valid. By returning None, we force a refresh + await client.set_gateway_wan_port_connect_state( port.port_number, enable, device, ipv6=ipv6 ) @@ -180,10 +184,13 @@ 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) + 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, @@ -197,7 +204,6 @@ GATEWAY_PORT_STATUS_SWITCHES: list[OmadaGatewayPortStatusSwitchEntityDescription 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", @@ -205,7 +211,6 @@ GATEWAY_PORT_STATUS_SWITCHES: list[OmadaGatewayPortStatusSwitchEntityDescription 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, ), ] @@ -230,7 +235,6 @@ class OmadaDevicePortSwitchEntity( """Generic toggle switch entity for a Netork Port of an Omada Device.""" _attr_has_entity_name = True - _port_details: TPort | None = None entity_description: OmadaDevicePortSwitchEntityDescription[ TCoordinator, TDevice, TPort ] @@ -239,7 +243,8 @@ class OmadaDevicePortSwitchEntity( self, coordinator: TCoordinator, device: TDevice, - port_id: int | str, + port_details: TPort, + port_id: str, entity_description: OmadaDevicePortSwitchEntityDescription[ TCoordinator, TDevice, TPort ], @@ -249,9 +254,9 @@ class OmadaDevicePortSwitchEntity( super().__init__(coordinator, device) self.entity_description = entity_description self._device = device - self._port_id = port_id + self._port_details = port_details self._attr_unique_id = f"{device.mac}_{port_id}_{entity_description.key}" - self._attr_translation_placeholders = {"port_name": port_name or str(port_id)} + self._attr_translation_placeholders = {"port_name": port_name or port_id} async def async_added_to_hass(self) -> None: """When entity is added to hass.""" @@ -259,18 +264,17 @@ class OmadaDevicePortSwitchEntity( self._do_update() 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 - ) + updated_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 + if updated_details: + self._port_details = updated_details + self._attr_is_on = self.entity_description.update_func(self._port_details) + else: 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() + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" @@ -290,12 +294,12 @@ class OmadaDevicePortSwitchEntity( ) def _do_update(self) -> None: - port = self.entity_description.coordinator_update_func( - self.coordinator, self._device, self._port_id + latest_port_details = self.entity_description.coordinator_update_func( + self.coordinator, self._device, self._port_details ) - if port: - self._port_details = port - self._attr_is_on = self.entity_description.update_func(port) + if latest_port_details: + self._port_details = latest_port_details + self._attr_is_on = self.entity_description.update_func(self._port_details) @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index d82b922f586..5695e434eff 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -152,9 +152,8 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) for identifier in device.identifiers - if identifier[0] == DOMAIN } if not dev_ids: return diff --git a/homeassistant/components/traccar_server/__init__.py b/homeassistant/components/traccar_server/__init__.py index fc513136681..c7a65d2d4a8 100644 --- a/homeassistant/components/traccar_server/__init__.py +++ b/homeassistant/components/traccar_server/__init__.py @@ -30,7 +30,11 @@ from .const import ( ) from .coordinator import TraccarServerCoordinator -PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/traccar_server/binary_sensor.py b/homeassistant/components/traccar_server/binary_sensor.py new file mode 100644 index 00000000000..6ee5757dcea --- /dev/null +++ b/homeassistant/components/traccar_server/binary_sensor.py @@ -0,0 +1,99 @@ +"""Support for Traccar server binary sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, Literal, TypeVar, cast + +from pytraccar import DeviceModel + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 .coordinator import TraccarServerCoordinator +from .entity import TraccarServerEntity + +_T = TypeVar("_T") + + +@dataclass(frozen=True, kw_only=True) +class TraccarServerBinarySensorEntityDescription( + Generic[_T], BinarySensorEntityDescription +): + """Describe Traccar Server sensor entity.""" + + data_key: Literal["position", "device", "geofence", "attributes"] + entity_registry_enabled_default = False + entity_category = EntityCategory.DIAGNOSTIC + value_fn: Callable[[_T], bool | None] + + +TRACCAR_SERVER_BINARY_SENSOR_ENTITY_DESCRIPTIONS = ( + TraccarServerBinarySensorEntityDescription[DeviceModel]( + key="attributes.motion", + data_key="position", + translation_key="motion", + device_class=BinarySensorDeviceClass.MOTION, + value_fn=lambda x: x["attributes"].get("motion", False), + ), + TraccarServerBinarySensorEntityDescription[DeviceModel]( + key="status", + data_key="device", + translation_key="status", + value_fn=lambda x: None if (s := x["status"]) == "unknown" else s == "online", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensor entities.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + TraccarServerBinarySensor( + coordinator=coordinator, + device=entry["device"], + description=cast(TraccarServerBinarySensorEntityDescription, description), + ) + for entry in coordinator.data.values() + for description in TRACCAR_SERVER_BINARY_SENSOR_ENTITY_DESCRIPTIONS + ) + + +class TraccarServerBinarySensor(TraccarServerEntity, BinarySensorEntity): + """Represent a traccar server binary sensor.""" + + _attr_has_entity_name = True + entity_description: TraccarServerBinarySensorEntityDescription + + def __init__( + self, + coordinator: TraccarServerCoordinator, + device: DeviceModel, + description: TraccarServerBinarySensorEntityDescription[_T], + ) -> None: + """Initialize the Traccar Server sensor.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = ( + f"{device['uniqueId']}_{description.data_key}_{description.key}" + ) + + @property + def is_on(self) -> bool | None: + """Return if the binary sensor is on or not.""" + return self.entity_description.value_fn( + getattr(self, f"traccar_{self.entity_description.data_key}") + ) diff --git a/homeassistant/components/traccar_server/config_flow.py b/homeassistant/components/traccar_server/config_flow.py index 0fa97c8100e..678bcc461e7 100644 --- a/homeassistant/components/traccar_server/config_flow.py +++ b/homeassistant/components/traccar_server/config_flow.py @@ -9,6 +9,7 @@ from pytraccar import ApiClient, ServerModel, TraccarException import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -111,7 +112,7 @@ OPTIONS_FLOW = { } -class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TraccarServerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Traccar Server.""" async def _get_server_info(self, user_input: dict[str, Any]) -> ServerModel: @@ -130,7 +131,7 @@ class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -162,7 +163,7 @@ class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, import_info: Mapping[str, Any] - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Import an entry.""" configured_port = str(import_info[CONF_PORT]) self._async_abort_entries_match( diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py index e459cdacf14..e7dba3ad99d 100644 --- a/homeassistant/components/traccar_server/device_tracker.py +++ b/homeassistant/components/traccar_server/device_tracker.py @@ -9,18 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTR_ADDRESS, - ATTR_ALTITUDE, - ATTR_CATEGORY, - ATTR_GEOFENCE, - ATTR_MOTION, - ATTR_SPEED, - ATTR_STATUS, - ATTR_TRACCAR_ID, - ATTR_TRACKER, - DOMAIN, -) +from .const import ATTR_CATEGORY, ATTR_TRACCAR_ID, ATTR_TRACKER, DOMAIN from .coordinator import TraccarServerCoordinator from .entity import TraccarServerEntity @@ -44,24 +33,12 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity): _attr_has_entity_name = True _attr_name = None - @property - def battery_level(self) -> int: - """Return battery value of the device.""" - return self.traccar_position["attributes"].get("batteryLevel", -1) - @property def extra_state_attributes(self) -> dict[str, Any]: """Return device specific attributes.""" - geofence_name = self.traccar_geofence["name"] if self.traccar_geofence else None return { **self.traccar_attributes, - ATTR_ADDRESS: self.traccar_position["address"], - ATTR_ALTITUDE: self.traccar_position["altitude"], ATTR_CATEGORY: self.traccar_device["category"], - ATTR_GEOFENCE: geofence_name, - ATTR_MOTION: self.traccar_position["attributes"].get("motion", False), - ATTR_SPEED: self.traccar_position["speed"], - ATTR_STATUS: self.traccar_device["status"], ATTR_TRACCAR_ID: self.traccar_device["id"], ATTR_TRACKER: DOMAIN, } diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index ea861a9bffa..68f1e4fca8a 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.diagnostics import REDACTED, async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant @@ -13,21 +13,23 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN from .coordinator import TraccarServerCoordinator -TO_REDACT = { +KEYS_TO_REDACT = { + "area", # This is the polygon area of a geofence CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE, - "area", # This is the polygon area of a geofence } def _entity_state( hass: HomeAssistant, entity: er.RegistryEntry, + coordinator: TraccarServerCoordinator, ) -> dict[str, Any] | None: + states_to_redact = {x["position"]["address"] for x in coordinator.data.values()} return ( { - "state": state.state, + "state": state.state if state.state not in states_to_redact else REDACTED, "attributes": state.attributes, } if (state := hass.states.get(entity.entity_id)) @@ -55,14 +57,15 @@ async def async_get_config_entry_diagnostics( "coordinator_data": coordinator.data, "entities": [ { - "enity_id": entity.entity_id, + "entity_id": entity.entity_id, "disabled": entity.disabled, - "state": _entity_state(hass, entity), + "unit_of_measurement": entity.unit_of_measurement, + "state": _entity_state(hass, entity, coordinator), } for entity in entities ], }, - TO_REDACT, + KEYS_TO_REDACT, ) @@ -81,6 +84,7 @@ async def async_get_device_diagnostics( include_disabled_entities=True, ) + await hass.config_entries.async_reload(entry.entry_id) return async_redact_data( { "subscription_status": coordinator.client.subscription_status, @@ -88,12 +92,13 @@ async def async_get_device_diagnostics( "coordinator_data": coordinator.data, "entities": [ { - "enity_id": entity.entity_id, + "entity_id": entity.entity_id, "disabled": entity.disabled, - "state": _entity_state(hass, entity), + "unit_of_measurement": entity.unit_of_measurement, + "state": _entity_state(hass, entity, coordinator), } for entity in entities ], }, - TO_REDACT, + KEYS_TO_REDACT, ) diff --git a/homeassistant/components/traccar_server/icons.json b/homeassistant/components/traccar_server/icons.json new file mode 100644 index 00000000000..a10b154fbff --- /dev/null +++ b/homeassistant/components/traccar_server/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "binary_sensor": { + "status": { + "default": "mdi:access-point-minus", + "state": { + "off": "mdi:access-point-off", + "on": "mdi:access-point" + } + } + }, + "sensor": { + "altitude": { + "default": "mdi:altimeter" + }, + "address": { + "default": "mdi:map-marker" + }, + "geofence": { + "default": "mdi:map-marker" + } + } + } +} diff --git a/homeassistant/components/traccar_server/sensor.py b/homeassistant/components/traccar_server/sensor.py new file mode 100644 index 00000000000..7f46399eb3f --- /dev/null +++ b/homeassistant/components/traccar_server/sensor.py @@ -0,0 +1,125 @@ +"""Support for Traccar server sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, Literal, TypeVar, cast + +from pytraccar import DeviceModel, GeofenceModel, PositionModel + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfSpeed +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import TraccarServerCoordinator +from .entity import TraccarServerEntity + +_T = TypeVar("_T") + + +@dataclass(frozen=True, kw_only=True) +class TraccarServerSensorEntityDescription(Generic[_T], SensorEntityDescription): + """Describe Traccar Server sensor entity.""" + + data_key: Literal["position", "device", "geofence", "attributes"] + entity_registry_enabled_default = False + entity_category = EntityCategory.DIAGNOSTIC + value_fn: Callable[[_T], StateType] + + +TRACCAR_SERVER_SENSOR_ENTITY_DESCRIPTIONS = ( + TraccarServerSensorEntityDescription[PositionModel]( + key="attributes.batteryLevel", + data_key="position", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + value_fn=lambda x: x["attributes"].get("batteryLevel", -1), + ), + TraccarServerSensorEntityDescription[PositionModel]( + key="speed", + data_key="position", + device_class=SensorDeviceClass.SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KNOTS, + suggested_display_precision=0, + value_fn=lambda x: x["speed"], + ), + TraccarServerSensorEntityDescription[PositionModel]( + key="altitude", + data_key="position", + translation_key="altitude", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.METERS, + suggested_display_precision=1, + value_fn=lambda x: x["altitude"], + ), + TraccarServerSensorEntityDescription[PositionModel]( + key="address", + data_key="position", + translation_key="address", + value_fn=lambda x: x["address"], + ), + TraccarServerSensorEntityDescription[GeofenceModel | None]( + key="name", + data_key="geofence", + translation_key="geofence", + value_fn=lambda x: x["name"] if x else None, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor entities.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + TraccarServerSensor( + coordinator=coordinator, + device=entry["device"], + description=cast(TraccarServerSensorEntityDescription, description), + ) + for entry in coordinator.data.values() + for description in TRACCAR_SERVER_SENSOR_ENTITY_DESCRIPTIONS + ) + + +class TraccarServerSensor(TraccarServerEntity, SensorEntity): + """Represent a tracked device.""" + + _attr_has_entity_name = True + entity_description: TraccarServerSensorEntityDescription + + def __init__( + self, + coordinator: TraccarServerCoordinator, + device: DeviceModel, + description: TraccarServerSensorEntityDescription[_T], + ) -> None: + """Initialize the Traccar Server sensor.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = ( + f"{device['uniqueId']}_{description.data_key}_{description.key}" + ) + + @property + def native_value(self) -> StateType: + """Return the value of the sensor.""" + return self.entity_description.value_fn( + getattr(self, f"traccar_{self.entity_description.data_key}") + ) diff --git a/homeassistant/components/traccar_server/strings.json b/homeassistant/components/traccar_server/strings.json index 87da7e8cdd1..8bec4b112ac 100644 --- a/homeassistant/components/traccar_server/strings.json +++ b/homeassistant/components/traccar_server/strings.json @@ -41,5 +41,34 @@ } } } + }, + "entity": { + "binary_sensor": { + "motion": { + "name": "Motion", + "state": { + "off": "Stopped", + "on": "Moving" + } + }, + "status": { + "name": "Status", + "state": { + "off": "Offline", + "on": "Online" + } + } + }, + "sensor": { + "address": { + "name": "Address" + }, + "altitude": { + "name": "Altitude" + }, + "geofence": { + "name": "Geofence" + } + } } } diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 17fdf20368a..03b1845d6a8 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -68,9 +68,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop) return True diff --git a/homeassistant/components/tractive/diagnostics.py b/homeassistant/components/tractive/diagnostics.py index f2bc80c51a1..cd1f5632f46 100644 --- a/homeassistant/components/tractive/diagnostics.py +++ b/homeassistant/components/tractive/diagnostics.py @@ -20,12 +20,10 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" trackables = hass.data[DOMAIN][config_entry.entry_id][TRACKABLES] - diagnostics_data = async_redact_data( + return async_redact_data( { "config_entry": config_entry.as_dict(), "trackables": [item.trackable for item in trackables], }, TO_REDACT, ) - - return diagnostics_data diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 4dcc4d41950..d7d6ae4ea0c 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -267,9 +267,6 @@ async def get_api( path=path, ) ) - _LOGGER.debug("Successfully connected to %s", host) - return api - except TransmissionAuthError as error: _LOGGER.error("Credentials for Transmission client are not valid") raise AuthenticationError from error @@ -279,3 +276,5 @@ async def get_api( except TransmissionError as error: _LOGGER.error(error) raise UnknownError from error + _LOGGER.debug("Successfully connected to %s", host) + return api diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 526228c2be1..2b70e2394f0 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -31,14 +31,11 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 8ea4617bbf3..3055bf46ca7 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -782,7 +782,7 @@ class SpeechManager: return filename - audio_task = self.hass.async_create_task(get_tts_data()) + audio_task = self.hass.async_create_task(get_tts_data(), eager_start=False) def handle_error(_future: asyncio.Future) -> None: """Handle error.""" diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 8ff7041fd5e..8b161f0538f 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -45,19 +45,19 @@ class IntegerTypeData: """Return the step scaled.""" return self.step / (10**self.scale) - def scale_value(self, value: float | int) -> float: + def scale_value(self, value: float) -> float: """Scale a value.""" return value / (10**self.scale) - def scale_value_back(self, value: float | int) -> int: + def scale_value_back(self, value: float) -> int: """Return raw value for scaled.""" return int(value * (10**self.scale)) def remap_value_to( self, value: float, - to_min: float | int = 0, - to_max: float | int = 255, + to_min: float = 0, + to_max: float = 255, reverse: bool = False, ) -> float: """Remap a value from this range to a new range.""" @@ -66,8 +66,8 @@ class IntegerTypeData: def remap_value_from( self, value: float, - from_min: float | int = 0, - from_max: float | int = 255, + from_min: float = 0, + from_max: float = 255, reverse: bool = False, ) -> float: """Remap a value from its current range to this range.""" diff --git a/homeassistant/components/tuya/util.py b/homeassistant/components/tuya/util.py index b6e6f17f49b..c1615b89c2d 100644 --- a/homeassistant/components/tuya/util.py +++ b/homeassistant/components/tuya/util.py @@ -4,11 +4,11 @@ from __future__ import annotations def remap_value( - value: float | int, - from_min: float | int = 0, - from_max: float | int = 255, - to_min: float | int = 0, - to_max: float | int = 255, + value: float, + from_min: float = 0, + from_max: float = 255, + to_min: float = 0, + to_max: float = 255, reverse: bool = False, ) -> float: """Remap a value from its current range, to a new range.""" diff --git a/homeassistant/components/twitch/config_flow.py b/homeassistant/components/twitch/config_flow.py index f9e121f3a17..146d2f39088 100644 --- a/homeassistant/components/twitch/config_flow.py +++ b/homeassistant/components/twitch/config_flow.py @@ -8,17 +8,13 @@ from typing import Any, cast from twitchAPI.helper import first from twitchAPI.twitch import Twitch -from twitchAPI.type import AuthScope, InvalidTokenException 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 +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN 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 -from .const import CONF_CHANNELS, CONF_REFRESH_TOKEN, DOMAIN, LOGGER, OAUTH_SCOPES +from .const import CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES class OAuth2FlowHandler( @@ -121,77 +117,3 @@ class OAuth2FlowHandler( 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]) -> ConfigFlowResult: - """Import from yaml.""" - client = await Twitch( - app_id=config[CONF_CLIENT_ID], - authenticate_app=False, - ) - client.auto_refresh_auth = False - token = config[CONF_TOKEN] - try: - await client.set_user_authentication( - token, validate=True, scope=[AuthScope.USER_READ_SUBSCRIPTIONS] - ) - except InvalidTokenException: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_invalid_token", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_invalid_token", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - return self.async_abort(reason="invalid_token") - user = await first(client.get_users()) - assert user - await self.async_set_unique_id(user.id) - try: - self._abort_if_unique_id_configured() - except AbortFlow as err: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_already_imported", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_already_imported", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - raise err - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - return self.async_create_entry( - title=user.display_name, - data={ - "auth_implementation": DOMAIN, - CONF_TOKEN: { - CONF_ACCESS_TOKEN: token, - CONF_REFRESH_TOKEN: "", - "expires_at": 0, - }, - "imported": True, - }, - options={CONF_CHANNELS: config[CONF_CHANNELS]}, - ) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 1107513080a..bcd9e95a1ae 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -10,34 +10,15 @@ from twitchAPI.twitch import ( TwitchResourceNotFound, TwitchUser, ) -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session -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 .const import CLIENT, CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES, SESSION -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_TOKEN): cv.string, - } -) - - ATTR_GAME = "game" ATTR_TITLE = "title" ATTR_SUBSCRIPTION = "subscribed" @@ -59,40 +40,6 @@ def chunk_list(lst: list, chunk_size: int) -> list[list]: return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)] -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Twitch platform.""" - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]), - ) - if CONF_TOKEN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - else: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_credentials_imported", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_credentials_imported", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/twitch/strings.json b/homeassistant/components/twitch/strings.json index f4128a15adc..bbe46526c36 100644 --- a/homeassistant/components/twitch/strings.json +++ b/homeassistant/components/twitch/strings.json @@ -16,19 +16,5 @@ "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" } - }, - "issues": { - "deprecated_yaml_invalid_token": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration couldn't be imported because the token in the configuration.yaml was invalid.\n\nPlease add Twitch again via the UI.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "deprecated_yaml_credentials_imported": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour application credentials are imported, but a config entry could not be created because there was no access token.\n\nPlease add Twitch again via the UI.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "deprecated_yaml_already_imported": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour application credentials are already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index 0aebea84c7d..b728059d0be 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -106,8 +106,7 @@ class UbusDeviceScanner(DeviceScanner): if self.mac2name is None: # Generation of mac2name dictionary failed return None - name = self.mac2name.get(device.upper(), None) - return name + return self.mac2name.get(device.upper(), None) async def async_get_extra_attributes(self, device: str) -> dict[str, str]: """Return the host to distinguish between multiple routers.""" diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 24a88724add..134dd675163 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -284,5 +284,4 @@ def _delta_mins(hhmm_time_str): if hhmm_datetime < now: hhmm_datetime += timedelta(days=1) - delta_mins = (hhmm_datetime - now).total_seconds() // 60 - return delta_mins + return (hhmm_datetime - now).total_seconds() // 60 diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5174a1a7796..69a6ec423ae 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -73,6 +74,18 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return await hub.async_reset() +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove config entry from a device.""" + hub: UnifiHub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + return not any( + identifier + for _, identifier in device_entry.connections + if identifier in hub.api.clients or identifier in hub.api.devices + ) + + class UnifiWirelessClients: """Class to store clients known to be wireless. diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 45fc76c73df..86c38a5bf3d 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -7,12 +7,14 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass +import secrets from typing import Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.devices import Devices from aiounifi.interfaces.ports import Ports +from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.device import ( Device, @@ -20,6 +22,7 @@ from aiounifi.models.device import ( DeviceRestartRequest, ) from aiounifi.models.port import Port +from aiounifi.models.wlan import Wlan, WlanChangePasswordRequest from homeassistant.components.button import ( ButtonDeviceClass, @@ -37,6 +40,8 @@ from .entity import ( UnifiEntityDescription, async_device_available_fn, async_device_device_info_fn, + async_wlan_available_fn, + async_wlan_device_info_fn, ) from .hub import UnifiHub @@ -56,6 +61,15 @@ async def async_power_cycle_port_control_fn( await api.request(DevicePowerCyclePortRequest.create(mac, int(index))) +async def async_regenerate_password_control_fn( + api: aiounifi.Controller, obj_id: str +) -> None: + """Regenerate WLAN password.""" + await api.request( + WlanChangePasswordRequest.create(obj_id, secrets.token_urlsafe(15)) + ) + + @dataclass(frozen=True, kw_only=True) class UnifiButtonEntityDescription( ButtonEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] @@ -91,6 +105,19 @@ ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = ( 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}", ), + UnifiButtonEntityDescription[Wlans, Wlan]( + key="WLAN regenerate password", + device_class=ButtonDeviceClass.UPDATE, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + api_handler_fn=lambda api: api.wlans, + available_fn=async_wlan_available_fn, + control_fn=async_regenerate_password_control_fn, + device_info_fn=async_wlan_device_info_fn, + name_fn=lambda wlan: "Regenerate Password", + object_fn=lambda api, obj_id: api.wlans[obj_id], + unique_id_fn=lambda hub, obj_id: f"regenerate_password-{obj_id}", + ), ) @@ -109,7 +136,7 @@ async def async_setup_entry( class UnifiButtonEntity(UnifiEntity[HandlerT, ApiItemT], ButtonEntity): - """Base representation of a UniFi image.""" + """Base representation of a UniFi button.""" entity_description: UnifiButtonEntityDescription[HandlerT, ApiItemT] diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 96a8a5dc1f8..dc48b9c31fe 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -240,7 +240,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): self._ignore_events = False self._is_connected = description.is_connected_fn(self.hub, self._obj_id) if self.is_connected: - self.hub.async_heartbeat( + self.hub.update_heartbeat( self.unique_id, dt_util.utcnow() + description.heartbeat_timedelta_fn(self.hub, self._obj_id), @@ -301,12 +301,12 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): # From unifi.entity.async_signal_reachable_callback # Controller connection state has changed and entity is unavailable # Cancel heartbeat - self.hub.async_heartbeat(self.unique_id) + self.hub.remove_heartbeat(self.unique_id) return if is_connected := description.is_connected_fn(self.hub, self._obj_id): self._is_connected = is_connected - self.hub.async_heartbeat( + self.hub.update_heartbeat( self.unique_id, dt_util.utcnow() + description.heartbeat_timedelta_fn(self.hub, self._obj_id), @@ -319,12 +319,12 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): return if event.key in self._event_is_on: - self.hub.async_heartbeat(self.unique_id) + self.hub.remove_heartbeat(self.unique_id) self._is_connected = True self.async_write_ha_state() return - self.hub.async_heartbeat( + self.hub.update_heartbeat( self.unique_id, dt_util.utcnow() + self.entity_description.heartbeat_timedelta_fn(self.hub, self._obj_id), @@ -344,7 +344,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): async def async_will_remove_from_hass(self) -> None: """Disconnect object when removed.""" await super().async_will_remove_from_hass() - self.hub.async_heartbeat(self.unique_id) + self.hub.remove_heartbeat(self.unique_id) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -359,6 +359,4 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): if self.is_connected: attributes_to_check = CLIENT_CONNECTED_ALL_ATTRIBUTES - attributes = {k: raw[k] for k in attributes_to_check if k in raw} - - return attributes + return {k: raw[k] for k in attributes_to_check if k in raw} diff --git a/homeassistant/components/unifi/hub/api.py b/homeassistant/components/unifi/hub/api.py index 8a1be0427b2..acdd941dd15 100644 --- a/homeassistant/components/unifi/hub/api.py +++ b/homeassistant/components/unifi/hub/api.py @@ -56,7 +56,6 @@ async def get_unifi_api( try: async with asyncio.timeout(10): await api.login() - return api except aiounifi.Unauthorized as err: LOGGER.warning( @@ -90,3 +89,5 @@ async def get_unifi_api( except aiounifi.AiounifiException as err: LOGGER.exception("Unknown UniFi Network communication error occurred: %s", err) raise AuthenticationRequired from err + + return api diff --git a/homeassistant/components/unifi/hub/entity_helper.py b/homeassistant/components/unifi/hub/entity_helper.py new file mode 100644 index 00000000000..c4bcf237386 --- /dev/null +++ b/homeassistant/components/unifi/hub/entity_helper.py @@ -0,0 +1,156 @@ +"""UniFi Network entity helper.""" + +from __future__ import annotations + +from datetime import datetime, timedelta + +import aiounifi +from aiounifi.models.device import DeviceSetPoePortModeRequest + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +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 + + +class UnifiEntityHelper: + """UniFi Network integration handling platforms for entity registration.""" + + def __init__(self, hass: HomeAssistant, api: aiounifi.Controller) -> None: + """Initialize the UniFi entity loader.""" + self.hass = hass + self.api = api + + self._device_command = UnifiDeviceCommand(hass, api) + self._heartbeat = UnifiEntityHeartbeat(hass) + + @callback + def reset(self) -> None: + """Cancel timers.""" + self._device_command.reset() + self._heartbeat.reset() + + @callback + def initialize(self) -> None: + """Initialize entity helper.""" + self._heartbeat.initialize() + + @property + def signal_heartbeat(self) -> str: + """Event to signal new heartbeat missed.""" + return self._heartbeat.signal + + @callback + def update_heartbeat(self, unique_id: str, heartbeat_expire_time: datetime) -> None: + """Update device time in heartbeat monitor.""" + self._heartbeat.update(unique_id, heartbeat_expire_time) + + @callback + def remove_heartbeat(self, unique_id: str) -> None: + """Update device time in heartbeat monitor.""" + self._heartbeat.remove(unique_id) + + @callback + def queue_poe_port_command( + self, device_id: str, port_idx: int, poe_mode: str + ) -> None: + """Queue commands to execute them together per device.""" + self._device_command.queue_poe_command(device_id, port_idx, poe_mode) + + +class UnifiEntityHeartbeat: + """UniFi entity heartbeat monitor.""" + + CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the heartbeat monitor.""" + self.hass = hass + + self._cancel_heartbeat_check: CALLBACK_TYPE | None = None + self._heartbeat_time: dict[str, datetime] = {} + + @callback + def reset(self) -> None: + """Cancel timers.""" + if self._cancel_heartbeat_check: + self._cancel_heartbeat_check() + self._cancel_heartbeat_check = None + + @callback + def initialize(self) -> None: + """Initialize heartbeat monitor.""" + self._cancel_heartbeat_check = async_track_time_interval( + self.hass, self._check_for_stale, self.CHECK_HEARTBEAT_INTERVAL + ) + + @property + def signal(self) -> str: + """Event to signal new heartbeat missed.""" + return "unifi-heartbeat-missed" + + @callback + def update(self, unique_id: str, heartbeat_expire_time: datetime) -> None: + """Update device time in heartbeat monitor.""" + self._heartbeat_time[unique_id] = heartbeat_expire_time + + @callback + def remove(self, unique_id: str) -> None: + """Remove device from heartbeat monitor.""" + self._heartbeat_time.pop(unique_id, None) + + @callback + def _check_for_stale(self, *_: datetime) -> None: + """Check for any devices scheduled to be marked disconnected.""" + now = dt_util.utcnow() + + unique_ids_to_remove = [] + for unique_id, heartbeat_expire_time in self._heartbeat_time.items(): + if now > heartbeat_expire_time: + async_dispatcher_send(self.hass, f"{self.signal}_{unique_id}") + unique_ids_to_remove.append(unique_id) + + for unique_id in unique_ids_to_remove: + del self._heartbeat_time[unique_id] + + +class UnifiDeviceCommand: + """UniFi Device command helper class.""" + + COMMAND_DELAY = 5 + + def __init__(self, hass: HomeAssistant, api: aiounifi.Controller) -> None: + """Initialize device command helper.""" + self.hass = hass + self.api = api + + self._command_queue: dict[str, dict[int, str]] = {} + self._cancel_command: CALLBACK_TYPE | None = None + + @callback + def reset(self) -> None: + """Cancel timers.""" + if self._cancel_command: + self._cancel_command() + self._cancel_command = None + + @callback + def queue_poe_command(self, device_id: str, port_idx: int, poe_mode: str) -> None: + """Queue commands to execute them together per device.""" + self.reset() + + device_queue = self._command_queue.setdefault(device_id, {}) + device_queue[port_idx] = poe_mode + + async def _command(now: datetime) -> None: + """Execute previously queued commands.""" + queue = self._command_queue.copy() + self._command_queue.clear() + for device_id, device_commands in queue.items(): + device = self.api.devices[device_id] + commands = list(device_commands.items()) + await self.api.request( + DeviceSetPoePortModeRequest.create(device, targets=commands) + ) + + self._cancel_command = async_call_later(self.hass, self.COMMAND_DELAY, _command) diff --git a/homeassistant/components/unifi/hub/hub.py b/homeassistant/components/unifi/hub/hub.py index df91584f267..f8c1f2517a2 100644 --- a/homeassistant/components/unifi/hub/hub.py +++ b/homeassistant/components/unifi/hub/hub.py @@ -2,13 +2,12 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime import aiounifi -from aiounifi.models.device import DeviceSetPoePortModeRequest from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( DeviceEntry, @@ -16,16 +15,13 @@ from homeassistant.helpers.device_registry import ( DeviceInfo, ) 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_SITE_ID, DOMAIN as UNIFI_DOMAIN, PLATFORMS from .config import UnifiConfig +from .entity_helper import UnifiEntityHelper from .entity_loader import UnifiEntityLoader from .websocket import UnifiWebsocket -CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) - class UnifiHub: """Manages a single UniFi Network instance.""" @@ -38,17 +34,12 @@ class UnifiHub: self.api = api self.config = UnifiConfig.from_config_entry(config_entry) self.entity_loader = UnifiEntityLoader(self) + self._entity_helper = UnifiEntityHelper(hass, api) self.websocket = UnifiWebsocket(hass, api, self.signal_reachable) 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.poe_command_queue: dict[str, dict[int, str]] = {} - self._cancel_poe_command: CALLBACK_TYPE | None = None - @callback @staticmethod def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> UnifiHub: @@ -61,6 +52,28 @@ class UnifiHub: """Websocket connection state.""" return self.websocket.available + @property + def signal_heartbeat_missed(self) -> str: + """Event to signal new heartbeat missed.""" + return self._entity_helper.signal_heartbeat + + @callback + def update_heartbeat(self, unique_id: str, heartbeat_expire_time: datetime) -> None: + """Update device time in heartbeat monitor.""" + self._entity_helper.update_heartbeat(unique_id, heartbeat_expire_time) + + @callback + def remove_heartbeat(self, unique_id: str) -> None: + """Update device time in heartbeat monitor.""" + self._entity_helper.remove_heartbeat(unique_id) + + @callback + def queue_poe_port_command( + self, device_id: str, port_idx: int, poe_mode: str + ) -> None: + """Queue commands to execute them together per device.""" + self._entity_helper.queue_poe_port_command(device_id, port_idx, poe_mode) + @property def signal_reachable(self) -> str: """Integration specific event to signal a change in connection status.""" @@ -71,77 +84,16 @@ class UnifiHub: """Event specific per UniFi entry to signal new options.""" return f"unifi-options-{self.config.entry.entry_id}" - @property - def signal_heartbeat_missed(self) -> str: - """Event specific per UniFi device tracker to signal new heartbeat missed.""" - return "unifi-heartbeat-missed" - async def initialize(self) -> None: """Set up a UniFi Network instance.""" await self.entity_loader.initialize() + self._entity_helper.initialize() assert self.config.entry.unique_id is not None self.is_admin = self.api.sites[self.config.entry.unique_id].role == "admin" 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 - ) - - @callback - def async_heartbeat( - self, unique_id: str, heartbeat_expire_time: datetime | None = None - ) -> None: - """Signal when a device has fresh home state.""" - if heartbeat_expire_time is not None: - self._heartbeat_time[unique_id] = heartbeat_expire_time - return - - if unique_id in self._heartbeat_time: - del self._heartbeat_time[unique_id] - - @callback - def _async_check_for_stale(self, *_: datetime) -> None: - """Check for any devices scheduled to be marked disconnected.""" - now = dt_util.utcnow() - - unique_ids_to_remove = [] - for unique_id, heartbeat_expire_time in self._heartbeat_time.items(): - if now > heartbeat_expire_time: - async_dispatcher_send( - self.hass, f"{self.signal_heartbeat_missed}_{unique_id}" - ) - unique_ids_to_remove.append(unique_id) - - for unique_id in unique_ids_to_remove: - del self._heartbeat_time[unique_id] - - @callback - def async_queue_poe_port_command( - self, device_id: str, port_idx: int, poe_mode: str - ) -> None: - """Queue commands to execute them together per device.""" - if self._cancel_poe_command: - self._cancel_poe_command() - self._cancel_poe_command = None - - device_queue = self.poe_command_queue.setdefault(device_id, {}) - device_queue[port_idx] = poe_mode - - async def async_execute_command(now: datetime) -> None: - """Execute previously queued commands.""" - queue = self.poe_command_queue.copy() - self.poe_command_queue.clear() - for device_id, device_commands in queue.items(): - device = self.api.devices[device_id] - commands = list(device_commands.items()) - await self.api.request( - DeviceSetPoePortModeRequest.create(device, targets=commands) - ) - - self._cancel_poe_command = async_call_later(self.hass, 5, async_execute_command) - @property def device_info(self) -> DeviceInfo: """UniFi Network device info.""" @@ -205,12 +157,6 @@ class UnifiHub: if not unload_ok: return False - if self._cancel_heartbeat_check: - self._cancel_heartbeat_check() - self._cancel_heartbeat_check = None - - if self._cancel_poe_command: - self._cancel_poe_command() - self._cancel_poe_command = None + self._entity_helper.reset() return True diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 305400a4b9d..982d654c8fe 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==75"], + "requirements": ["aiounifi==76"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index efb3eed4de4..17b3cae93fd 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -10,6 +10,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal +from functools import partial from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -32,7 +33,7 @@ from homeassistant.components.sensor import ( UnitOfTemperature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, UnitOfDataRate, UnitOfPower +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfDataRate, UnitOfPower from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -140,6 +141,16 @@ def async_device_outlet_supported_fn(hub: UnifiHub, obj_id: str) -> bool: return hub.api.devices[obj_id].outlet_ac_power_budget is not None +def device_system_stats_supported_fn( + stat_index: int, hub: UnifiHub, obj_id: str +) -> bool: + """Determine if a device supports reading item at index in system stats.""" + return ( + "system-stats" in hub.api.devices[obj_id].raw + and hub.api.devices[obj_id].system_stats[stat_index] != "" + ) + + @callback def async_client_is_connected_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client was last seen recently.""" @@ -228,6 +239,42 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( 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", ), + UnifiSensorEntityDescription[Ports, Port]( + key="Port Bandwidth sensor RX", + device_class=SensorDeviceClass.DATA_RATE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + icon="mdi:download", + allowed_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors, + api_handler_fn=lambda api: api.ports, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda port: f"{port.name} RX", + object_fn=lambda api, obj_id: api.ports[obj_id], + unique_id_fn=lambda hub, obj_id: f"port_rx-{obj_id}", + value_fn=lambda hub, port: port.rx_bytes_r, + ), + UnifiSensorEntityDescription[Ports, Port]( + key="Port Bandwidth sensor TX", + device_class=SensorDeviceClass.DATA_RATE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + icon="mdi:upload", + allowed_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors, + api_handler_fn=lambda api: api.ports, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda port: f"{port.name} TX", + object_fn=lambda api, obj_id: api.ports[obj_id], + unique_id_fn=lambda hub, obj_id: f"port_tx-{obj_id}", + value_fn=lambda hub, port: port.tx_bytes_r, + ), UnifiSensorEntityDescription[Clients, Client]( key="Client uptime", device_class=SensorDeviceClass.TIMESTAMP, @@ -339,6 +386,34 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( value_fn=async_device_state_value_fn, options=list(DEVICE_STATES.values()), ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device CPU utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda device: "CPU utilization", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(device_system_stats_supported_fn, 0), + unique_id_fn=lambda hub, obj_id: f"cpu_utilization-{obj_id}", + value_fn=lambda hub, device: device.system_stats[0], + ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device memory utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda device: "Memory utilization", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(device_system_stats_supported_fn, 1), + unique_id_fn=lambda hub, obj_id: f"memory_utilization-{obj_id}", + value_fn=lambda hub, device: device.system_stats[1], + ), ) @@ -385,7 +460,7 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): if description.is_connected_fn is not None: # Send heartbeat if client is connected if description.is_connected_fn(self.hub, self._obj_id): - self.hub.async_heartbeat( + self.hub.update_heartbeat( self._attr_unique_id, dt_util.utcnow() + self.hub.config.option_detection_time, ) @@ -410,4 +485,4 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): if self.entity_description.is_connected_fn is not None: # Remove heartbeat registration - self.hub.async_heartbeat(self._attr_unique_id) + self.hub.remove_heartbeat(self._attr_unique_id) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 6e073a655a5..45357dd67d2 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -147,7 +147,7 @@ async def async_poe_port_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> port = hub.api.ports[obj_id] on_state = "auto" if port.raw["poe_caps"] != 8 else "passthrough" state = on_state if target else "off" - hub.async_queue_poe_port_command(mac, int(index), state) + hub.queue_poe_port_command(mac, int(index), state) async def async_port_forward_control_fn( diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 71c887cd870..d85f91be860 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -110,9 +110,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) if not entry.options.get(CONF_ALLOW_EA, False) and ( @@ -154,7 +152,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: }, ) ir.async_delete_issue(hass, DOMAIN, "ea_channel_warning") - _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) + _LOGGER.exception("Error setting up UniFi Protect integration") raise return True diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c0a6d65ff7a..55ddf91d3cb 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -269,7 +269,12 @@ class ProtectData: this will be a no-op. If the websocket is disconnected, this will trigger a reconnect and refresh. """ - self._hass.async_create_task(self.async_refresh(), eager_start=True) + self._entry.async_create_background_task( + self._hass, + self.async_refresh(), + name=f"{DOMAIN} {self._entry.title} refresh", + eager_start=True, + ) @callback def async_subscribe_device_id( diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 32cac04797f..ba962891454 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -419,9 +419,7 @@ class ProtectMediaSource(MediaSource): if camera is not None: title = f"{camera.display_name} > {title}" - title = f"{data.api.bootstrap.nvr.display_name} > {title}" - - return title + return f"{data.api.bootstrap.nvr.display_name} > {title}" async def _build_event( self, @@ -868,7 +866,7 @@ class ProtectMediaSource(MediaSource): async def _build_console(self, data: ProtectData) -> BrowseMediaSource: """Build media source for a single UniFi Protect NVR.""" - base = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=f"{data.api.bootstrap.nvr.id}:browse", media_class=MediaClass.DIRECTORY, @@ -880,8 +878,6 @@ class ProtectMediaSource(MediaSource): children=await self._build_cameras(data), ) - return base - async def _build_sources(self) -> BrowseMediaSource: """Return all media source for all UniFi Protect NVRs.""" diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index 0aa7056976b..0f9bff63689 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -52,15 +52,13 @@ def async_generate_event_video_url(event: Event) -> str: raise ValueError("Event is ongoing") url_format = VideoProxyView.url or "{nvr_id}/{camera_id}/{start}/{end}" - url = url_format.format( + return url_format.format( nvr_id=event.api.bootstrap.nvr.id, camera_id=event.camera_id, start=event.start.replace(microsecond=0).isoformat(), end=event.end.replace(microsecond=0).isoformat(), ) - return url - @callback def _client_error(message: Any, code: HTTPStatus) -> web.Response: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 90036e5d47c..5deebc4103b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -79,13 +79,12 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, async_track_state_change_event, diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 79ed768282a..02b852ec3a6 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/upc_connect", "iot_class": "local_polling", "loggers": ["connect_box"], - "requirements": ["connect-box==0.2.8"] + "requirements": ["connect-box==0.3.1"] } diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index f274da6f412..57d63c92ede 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import lru_cache +from functools import cached_property, lru_cache import logging -from typing import TYPE_CHECKING, Any, Final, final +from typing import Any, Final, final from awesomeversion import AwesomeVersion, AwesomeVersionCompareException import voluptuous as vol @@ -43,11 +43,6 @@ from .const import ( UpdateEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(minutes=15) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" @@ -408,10 +403,10 @@ class UpdateEntity( try: newer = _version_is_newer(latest_version, installed_version) - return STATE_ON if newer else STATE_OFF except AwesomeVersionCompareException: # Can't compare versions, already tried exact match return STATE_ON + return STATE_ON if newer else STATE_OFF @final @property diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 4dff753ac6a..0b9eecb1b15 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -74,9 +74,7 @@ async def async_create_device(hass: HomeAssistant, location: str) -> Device: # Create profile wrapper. igd_device = IgdDevice(upnp_device, None) - device = Device(hass, igd_device) - - return device + return Device(hass, igd_device) class Device: diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py index 2cfec38d200..6dd68bae148 100644 --- a/homeassistant/components/uptime/config_flow.py +++ b/homeassistant/components/uptime/config_flow.py @@ -20,9 +20,6 @@ class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> 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="Uptime", diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index 2b4dbcd0fec..37e61976b9c 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/uptime", "integration_type": "service", "iot_class": "local_push", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/components/uptime/strings.json b/homeassistant/components/uptime/strings.json index 9ceb91de9ba..868c2a51588 100644 --- a/homeassistant/components/uptime/strings.json +++ b/homeassistant/components/uptime/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%]" } } } diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 48697c98ae7..46950ba5b91 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -207,12 +207,8 @@ 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, 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_STARTED, self.async_start) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) async def async_start(self, event: Event) -> None: """Start USB Discovery and run a manual scan.""" @@ -242,9 +238,7 @@ class USBDiscovery: def _stop_observer(event: Event) -> None: observer.stop() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _stop_observer, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_observer) self.observer_active = True def _get_monitor_observer(self) -> MonitorObserver | None: @@ -400,6 +394,7 @@ class USBDiscovery: cooldown=REQUEST_SCAN_COOLDOWN, immediate=True, function=self._async_scan, + background=True, ) await self._request_debouncer.async_call() diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index 71df5ba2c05..19269801c11 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["pyudev==0.23.2", "pyserial==3.5"] + "requirements": ["pyudev==0.24.1", "pyserial==3.5"] } diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 71df488de7e..4bacde32367 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -150,7 +150,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, {meter: {CONF_METER: meter}}, config, - ) + ), + eager_start=True, ) else: # create tariff selection @@ -161,7 +162,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, {CONF_METER: meter, CONF_TARIFFS: conf[CONF_TARIFFS]}, config, - ) + ), + eager_start=True, ) hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = ( @@ -180,7 +182,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.async_create_task( discovery.async_load_platform( hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config - ) + ), + eager_start=True, ) return True diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 014cd93b53b..223e54d7d9f 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -30,7 +30,13 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( device_registry as dr, entity_platform, @@ -40,7 +46,6 @@ 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.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 307db17c2b8..4615bc2990a 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -243,7 +243,7 @@ class UnifiVideoCamera(Camera): """Return the source of the stream.""" for channel in self._caminfo["channels"]: if channel["isRtspEnabled"]: - uri = next( + return next( ( uri for i, uri in enumerate(channel["rtspUris"]) @@ -251,7 +251,6 @@ class UnifiVideoCamera(Camera): if re.search(self._nvr._host, uri) ) ) - return uri return None diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index bdf690ed63f..4f5b6066dbd 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -5,9 +5,9 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta from enum import IntFlag -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -38,11 +38,6 @@ 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: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 7c9234d35c2..b8e94e9dfb7 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -175,11 +175,10 @@ class ValloxServiceHandler: try: await self._client.set_fan_speed(Profile.HOME, fan_speed) - return True - except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Home profile: %s", err) return False + return True async def async_set_profile_fan_speed_away( self, fan_speed: int = DEFAULT_FAN_SPEED_AWAY @@ -189,11 +188,10 @@ class ValloxServiceHandler: try: await self._client.set_fan_speed(Profile.AWAY, fan_speed) - return True - except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Away profile: %s", err) return False + return True async def async_set_profile_fan_speed_boost( self, fan_speed: int = DEFAULT_FAN_SPEED_BOOST @@ -203,11 +201,10 @@ class ValloxServiceHandler: try: await self._client.set_fan_speed(Profile.BOOST, fan_speed) - return True - except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Boost profile: %s", err) return False + return True async def async_handle(self, call: ServiceCall) -> None: """Dispatch a service call.""" diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index ea03c4b15f1..479b7f02024 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -145,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle a clear cache service call.""" # clear the cache with suppress(FileNotFoundError): - if CONF_ADDRESS in call.data and call.data[CONF_ADDRESS]: + if call.data.get(CONF_ADDRESS): await hass.async_add_executor_job( os.unlink, hass.config.path( diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index f37de104659..823d682d339 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -34,6 +34,7 @@ class VelbusCover(VelbusEntity, CoverEntity): """Representation a Velbus cover.""" _channel: VelbusBlind + _assumed_closed: bool def __init__(self, channel: VelbusBlind) -> None: """Initialize the cover.""" @@ -51,11 +52,16 @@ class VelbusCover(VelbusEntity, CoverEntity): | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP ) + self._attr_assumed_state = True + # guess the state to get the open/closed icons somewhat working + self._assumed_closed = False @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" - return self._channel.is_closed() + if self._channel.support_position(): + return self._channel.is_closed() + return self._assumed_closed @property def is_opening(self) -> bool: @@ -83,11 +89,13 @@ class VelbusCover(VelbusEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._channel.open() + self._assumed_closed = False @api_call async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._channel.close() + self._assumed_closed = True @api_call async def async_stop_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/velux/config_flow.py b/homeassistant/components/velux/config_flow.py index da6502b86da..679af4bd20a 100644 --- a/homeassistant/components/velux/config_flow.py +++ b/homeassistant/components/velux/config_flow.py @@ -5,7 +5,7 @@ from typing import Any from pyvlx import PyVLX, PyVLXException 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.core import DOMAIN as HOMEASSISTANT_DOMAIN import homeassistant.helpers.config_validation as cv @@ -21,12 +21,10 @@ DATA_SCHEMA = vol.Schema( ) -class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VeluxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for velux.""" - async def async_step_import( - self, config: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: """Import a config entry.""" def create_repair(error: str | None = None) -> None: @@ -81,7 +79,7 @@ class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index b2fd090e781..08badae8cd0 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -55,5 +55,6 @@ SKU_TO_BASE_DEVICE = { "LAP-V201S-WUS": "Vital200S", # Alt ID Model Vital200S "LAP-V201-AUSR": "Vital200S", # Alt ID Model Vital200S "Vital100S": "Vital100S", - "LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S, + "LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S + "LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S } diff --git a/homeassistant/components/vesync/diagnostics.py b/homeassistant/components/vesync/diagnostics.py index b56c8fc5db6..9af8a7fed67 100644 --- a/homeassistant/components/vesync/diagnostics.py +++ b/homeassistant/components/vesync/diagnostics.py @@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" manager: VeSync = hass.data[DOMAIN][VS_MANAGER] - data = { + return { DOMAIN: { "bulb_count": len(manager.bulbs), "fan_count": len(manager.fans), @@ -40,8 +40,6 @@ async def async_get_config_entry_diagnostics( }, } - return data - async def async_get_device_diagnostics( hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 1d8ea6463bf..6272c033b4f 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -184,6 +184,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.auto_mode() elif preset_mode == FAN_MODE_SLEEP: self.smartfan.sleep_mode() + elif preset_mode == FAN_MODE_PET: + self.smartfan.pet_mode() self.schedule_update_ha_state() diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 3738b0f956a..9c6c6bca422 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -84,11 +84,13 @@ async def async_http_request(hass, uri): if req.status != HTTPStatus.OK: return {"error": req.status} json_response = await req.json() - return json_response except (TimeoutError, aiohttp.ClientError) as exc: _LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc) + return None except ValueError: _LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint") + return None + return json_response class ViaggiaTrenoSensor(SensorEntity): diff --git a/homeassistant/components/vicare/number.py b/homeassistant/components/vicare/number.py index f92241ceace..c0564170274 100644 --- a/homeassistant/components/vicare/number.py +++ b/homeassistant/components/vicare/number.py @@ -49,6 +49,23 @@ class ViCareNumberEntityDescription(NumberEntityDescription, ViCareRequiredKeysM stepping_getter: Callable[[PyViCareDevice], float | None] | None = None +DEVICE_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = ( + ViCareNumberEntityDescription( + key="dhw_secondary_temperature", + translation_key="dhw_secondary_temperature", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_getter=lambda api: api.getDomesticHotWaterConfiguredTemperature2(), + value_setter=lambda api, value: api.setDomesticHotWaterTemperature2(value), + # no getters for min, max, stepping exposed yet, using static values + native_min_value=10, + native_max_value=60, + native_step=1, + ), +) + + CIRCUIT_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = ( ViCareNumberEntityDescription( key="heating curve shift", @@ -216,18 +233,32 @@ def _build_entities( ) -> list[ViCareNumber]: """Create ViCare number entities for a device.""" - return [ + entities: list[ViCareNumber] = [ ViCareNumber( - circuit, + device.api, device.config, description, ) for device in device_list - for circuit in get_circuits(device.api) - for description in CIRCUIT_ENTITY_DESCRIPTIONS - if is_supported(description.key, description, circuit) + for description in DEVICE_ENTITY_DESCRIPTIONS + if is_supported(description.key, description, device.api) ] + entities.extend( + [ + ViCareNumber( + circuit, + device.config, + description, + ) + for device in device_list + for circuit in get_circuits(device.api) + for description in CIRCUIT_ENTITY_DESCRIPTIONS + if is_supported(description.key, description, circuit) + ] + ) + return entities + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 5a69cae4d29..f81d01b71cf 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -89,6 +89,9 @@ }, "comfort_heating_temperature": { "name": "[%key:component::vicare::entity::number::comfort_temperature::name%]" + }, + "dhw_secondary_temperature": { + "name": "DHW secondary temperature" } }, "sensor": { diff --git a/homeassistant/components/vicare/utils.py b/homeassistant/components/vicare/utils.py index 2019f28a896..2ba5ddbfb0a 100644 --- a/homeassistant/components/vicare/utils.py +++ b/homeassistant/components/vicare/utils.py @@ -35,13 +35,14 @@ def is_supported( """Check if the PyViCare device supports the requested sensor.""" try: entity_description.value_getter(vicare_device) - _LOGGER.debug("Found entity %s", name) - return True except PyViCareNotSupportedFeatureError: _LOGGER.debug("Feature not supported %s", name) + return False except AttributeError as error: _LOGGER.debug("Feature not supported %s: %s", name, error) - return False + return False + _LOGGER.debug("Found entity %s", name) + return True def get_burners(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent]: diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index ced871b7616..7e2e974e709 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -4,7 +4,9 @@ "codeowners": ["@paoloantinori", "@chemelli74"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vodafone_station", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aiovodafone"], + "quality_scale": "silver", "requirements": ["aiovodafone==0.5.4"] } diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index 937c0220cbf..2a08a9b2ebe 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -107,12 +107,12 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="phone_num1", translation_key="phone_num1", - is_suitable=lambda info: info["phone_unavailable1"] == "0", + is_suitable=lambda info: info["phone_num1"] != "", ), VodafoneStationEntityDescription( key="phone_num2", translation_key="phone_num2", - is_suitable=lambda info: info["phone_unavailable2"] == "0", + is_suitable=lambda info: info["phone_num2"] != "", ), VodafoneStationEntityDescription( key="sys_uptime", diff --git a/homeassistant/components/voip/devices.py b/homeassistant/components/voip/devices.py index 9acc04f6879..4e2dca15308 100644 --- a/homeassistant/components/voip/devices.py +++ b/homeassistant/components/voip/devices.py @@ -84,7 +84,7 @@ class VoIPDevices: ) @callback - def async_device_removed(ev: Event) -> None: + def async_device_removed(ev: Event[dr.EventDeviceRegistryUpdatedData]) -> None: """Handle device removed.""" removed_id = ev.data["device_id"] self.devices = { @@ -97,7 +97,7 @@ class VoIPDevices: self.hass.bus.async_listen( dr.EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed, - callback(lambda event_data: event_data.get("action") == "remove"), + callback(lambda event_data: event_data["action"] == "remove"), ) ) diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 4d97720934c..5770d9d2b4a 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -445,9 +445,9 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(tts_seconds + self.tts_extra_timeout): # TTS audio is 16Khz 16-bit mono await self._async_send_audio(audio_bytes) - except TimeoutError as err: + except TimeoutError: _LOGGER.warning("TTS timeout") - raise err + raise finally: # Signal pipeline to restart self._tts_done.set() diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index a0b54fd8db0..e5c3a055310 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -129,7 +129,7 @@ class WolSwitch(SwitchEntity): if self._attr_assumed_state: self._state = True - self.async_write_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the device off if an off action is present.""" @@ -138,7 +138,7 @@ class WolSwitch(SwitchEntity): if self._attr_assumed_state: self._state = False - self.async_write_ha_state() + self.schedule_update_ha_state() def update(self) -> None: """Check if device is on and update the state. Only called if assumed state is false.""" diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 4725e92ca84..bf7c6d1f654 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -154,7 +154,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise wallbox_connection_error + raise async def async_set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" @@ -185,7 +185,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise wallbox_connection_error + raise async def async_set_lock_unlock(self, lock: bool) -> None: """Set wallbox to locked or unlocked.""" diff --git a/homeassistant/components/waqi/config_flow.py b/homeassistant/components/waqi/config_flow.py index 068cb1a5020..e7e7a536654 100644 --- a/homeassistant/components/waqi/config_flow.py +++ b/homeassistant/components/waqi/config_flow.py @@ -45,8 +45,8 @@ async def get_by_station_number( measuring_station = await client.get_by_station_number(station_number) except WAQIConnectionError: errors["base"] = "cannot_connect" - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception(exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return measuring_station, errors @@ -76,8 +76,8 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except WAQIConnectionError: errors["base"] = "cannot_connect" - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception(exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: self.data = user_input @@ -118,8 +118,8 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): ) except WAQIConnectionError: errors["base"] = "cannot_connect" - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception(exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return await self._async_create_entry(measuring_station) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 167acb85914..ad0149919dc 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -6,8 +6,9 @@ from collections.abc import Mapping from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -44,12 +45,6 @@ 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: - from homeassistant.backports.functools import cached_property - - DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index a196c5f4f57..d0f63b97b78 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.config_entries import ( @@ -119,6 +121,10 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Init Config Flow.""" + self._entry: ConfigEntry | None = None + @staticmethod @callback def async_get_options_flow( @@ -127,7 +133,9 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return WazeOptionsFlow(config_entry) - async def async_step_user(self, user_input=None) -> ConfigFlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} user_input = user_input or {} @@ -140,6 +148,13 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_DESTINATION], user_input[CONF_REGION], ): + if self._entry: + return self.async_update_reload_and_abort( + self._entry, + title=user_input[CONF_NAME], + data=user_input, + reason="reconfigure_successful", + ) return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input, @@ -155,3 +170,18 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input), errors=errors, ) + + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration.""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self._entry + + data = self._entry.data.copy() + data[CONF_REGION] = data[CONF_REGION].lower() + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, data), + ) diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index 2a5017a5b9f..e6dd3c3a22e 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -16,7 +16,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "options": { diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 404154ade2b..95655f439c9 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -6,10 +6,9 @@ import abc from collections.abc import Callable, Iterable from contextlib import suppress from datetime import timedelta -from functools import partial +from functools import cached_property, partial import logging from typing import ( - TYPE_CHECKING, Any, Final, Generic, @@ -84,12 +83,6 @@ from .const import ( ) from .websocket_api import async_setup as async_setup_ws_api -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) ATTR_CONDITION_CLASS = "condition_class" diff --git a/homeassistant/components/weather/significant_change.py b/homeassistant/components/weather/significant_change.py index d36139904f5..ce7bcd15ede 100644 --- a/homeassistant/components/weather/significant_change.py +++ b/homeassistant/components/weather/significant_change.py @@ -64,7 +64,7 @@ VALID_CARDINAL_DIRECTIONS: list[str] = [ ] -def _cardinal_to_degrees(value: str | int | float | None) -> int | float | None: +def _cardinal_to_degrees(value: str | float | None) -> int | float | None: """Translate a cardinal direction into azimuth angle (degrees).""" if not isinstance(value, str): return value diff --git a/homeassistant/components/weatherflow_cloud/config_flow.py b/homeassistant/components/weatherflow_cloud/config_flow.py index 6e6212042e1..e8972c320ed 100644 --- a/homeassistant/components/weatherflow_cloud/config_flow.py +++ b/homeassistant/components/weatherflow_cloud/config_flow.py @@ -9,7 +9,7 @@ from aiohttp import ClientResponseError import voluptuous as vol from weatherflow4py.api import WeatherFlowRestAPI -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN from .const import DOMAIN @@ -27,14 +27,14 @@ async def _validate_api_token(api_token: str) -> dict[str, Any]: return {} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WeatherFlowCloudConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for WeatherFlowCloud.""" VERSION = 1 async def async_step_reauth( self, user_input: Mapping[str, Any] - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a flow for reauth.""" errors = {} @@ -50,6 +50,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): existing_entry, data={CONF_API_TOKEN: api_token}, reason="reauth_successful", + reload_even_if_entry_is_unchanged=False, ) return self.async_show_form( @@ -60,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index f2d1416e2c9..0076c85e268 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -178,10 +178,10 @@ async def async_handle_webhook( response: Response | None = await webhook["handler"](hass, webhook_id, request) if response is None: response = Response(status=HTTPStatus.OK) - return response except Exception: # pylint: disable=broad-except _LOGGER.exception("Error processing webhook %s", webhook_id) return Response(status=HTTPStatus.OK) + return response async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 0175df5d828..17d92b1abf3 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -55,8 +55,7 @@ async def async_get_triggers( _hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for device.""" - triggers = [async_get_turn_on_trigger(device_id)] - return triggers + return [async_get_turn_on_trigger(device_id)] async def async_attach_trigger( diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 191ea1ea996..54539158148 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -21,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import ( Context, Event, + EventStateChangedData, HomeAssistant, ServiceResponse, State, @@ -36,7 +37,6 @@ from homeassistant.exceptions import ( from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, async_track_template_result, @@ -165,7 +165,7 @@ def handle_subscribe_events( ) connection.subscriptions[msg["id"]] = hass.bus.async_listen( - event_type, forward_events, run_immediately=True + event_type, forward_events ) connection.send_result(msg["id"]) @@ -289,7 +289,7 @@ async def handle_call_service( translation_placeholders=err.translation_placeholders, ) except HomeAssistantError as err: - connection.logger.exception(err) + connection.logger.exception("Unexpected exception") connection.send_error( msg["id"], const.ERR_HOME_ASSISTANT_ERROR, @@ -299,7 +299,7 @@ async def handle_call_service( translation_placeholders=err.translation_placeholders, ) except Exception as err: # pylint: disable=broad-except - connection.logger.exception(err) + connection.logger.exception("Unexpected exception") connection.send_error(msg["id"], const.ERR_UNKNOWN_ERROR, str(err)) @@ -410,7 +410,6 @@ def handle_subscribe_entities( connection.user, msg["id"], ), - run_immediately=True, ) connection.send_result(msg["id"]) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 63b4418a19d..3c0743601dd 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Hashable from contextvars import ContextVar -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from aiohttp import web import voluptuous as vol @@ -65,9 +65,9 @@ class ActiveConnection: self.last_id = 0 self.can_coalesce = False self.supported_features: dict[str, float] = {} - self.handlers: dict[str, tuple[MessageHandler, vol.Schema]] = self.hass.data[ - const.DOMAIN - ] + self.handlers: dict[str, tuple[MessageHandler, vol.Schema | Literal[False]]] = ( + self.hass.data[const.DOMAIN] + ) self.binary_handlers: list[BinaryHandler | None] = [] current_connection.set(self) @@ -185,6 +185,7 @@ class ActiveConnection: or ( not (cur_id := msg.get("id")) or type(cur_id) is not int # noqa: E721 + or cur_id < 0 or not (type_ := msg.get("type")) or type(type_) is not str # noqa: E721 ) @@ -220,7 +221,12 @@ class ActiveConnection: handler, schema = handler_schema try: - handler(self.hass, self, schema(msg)) + if schema is False: + if len(msg) > 2: + raise vol.Invalid("extra keys not allowed") + handler(self.hass, self, msg) + else: + handler(self.hass, self, schema(msg)) except Exception as err: # pylint: disable=broad-except self.async_handle_exception(msg, err) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 51643752a0f..0ed8be30139 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -137,7 +137,7 @@ def websocket_command( The schema must be either a dictionary where the keys are voluptuous markers, or a voluptuous.All schema where the first item is a voluptuous Mapping schema. """ - if isinstance(schema, dict): + if is_dict := isinstance(schema, dict): command = schema["type"] else: command = schema.validators[0].schema["type"] @@ -145,9 +145,13 @@ def websocket_command( def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" # pylint: disable=protected-access - if isinstance(schema, dict): + if is_dict and len(schema) == 1: # type only empty schema + func._ws_schema = False # type: ignore[attr-defined] + elif is_dict: func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] else: + if TYPE_CHECKING: + assert not isinstance(schema, dict) extended_schema = vol.All( schema.validators[0].extend( messages.BASE_COMMAND_MESSAGE_SCHEMA.schema diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 83d68ee21ea..fc75b46ddbd 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -292,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, run_immediately=True + EVENT_HOMEASSISTANT_STOP, self._async_handle_hass_stop ) writer = wsock._writer # pylint: disable=protected-access diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 8de43c57f00..75a9c9999d4 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -15,9 +15,8 @@ from homeassistant.const import ( COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, ) -from homeassistant.core import Event, State +from homeassistant.core import Event, EventStateChangedData, 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, diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 7d668466bc2..4d874bca74e 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -30,9 +30,12 @@ async def async_setup_platform( class APICount(SensorEntity): """Entity to represent how many people are connected to the stream API.""" + _attr_name = "Connected clients" + _attr_native_unit_of_measurement = "clients" + def __init__(self) -> None: """Initialize the API count.""" - self.count = 0 + self._attr_native_value = 0 async def async_added_to_hass(self) -> None: """Handle addition to hass.""" @@ -47,22 +50,7 @@ class APICount(SensorEntity): ) ) - @property - def name(self) -> str: - """Return name of entity.""" - return "Connected clients" - - @property - def native_value(self) -> int: - """Return current API count.""" - return self.count - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return "clients" - @callback def _update_count(self) -> None: - self.count = self.hass.data.get(DATA_CONNECTIONS, 0) + self._attr_native_value = self.hass.data.get(DATA_CONNECTIONS, 0) self.async_write_ha_state() diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 8a9a122c03c..7d068cbd5bf 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -96,9 +96,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: discovery_responder.stop() registry.stop() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) yaml_config = config.get(DOMAIN, {}) hass.data[DOMAIN] = WemoData( @@ -208,16 +206,14 @@ class WemoDispatcher: self._dispatch_backlog[platform] = [coordinator] 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) + if platforms_to_load: + await hass.config_entries.async_forward_entry_setups( + self._config_entry, platforms_to_load + ) + async def async_connect_platform( self, platform: Platform, dispatch: DispatchCallback ) -> None: diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 2a4185a7640..7326e0b42f5 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, fields from datetime import timedelta +from functools import partial import logging from typing import TYPE_CHECKING, Literal @@ -130,7 +131,14 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en ) else: updated = self.wemo.subscription_update(event_type, params) - self.hass.create_task(self._async_subscription_callback(updated)) + self.hass.loop.call_soon_threadsafe( + partial( + self.hass.async_create_background_task, + self._async_subscription_callback(updated), + f"{self.name} subscription_callback", + eager_start=True, + ) + ) async def async_shutdown(self) -> None: """Unregister push subscriptions and remove from coordinators dict.""" @@ -205,7 +213,7 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en except Exception as err: # pylint: disable=broad-except self.last_exception = err self.last_update_success = False - _LOGGER.exception("Unexpected error fetching %s data: %s", self.name, err) + _LOGGER.exception("Unexpected error fetching %s data", self.name) else: self.async_set_updated_data(None) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 665a90ec5a7..0b86a2b5201 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -17,14 +17,9 @@ from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response from aiowithings import NotificationCategory, WithingsClient from aiowithings.util import to_enum -import voluptuous as vol from yarl import URL from homeassistant.components import cloud -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.components.http import HomeAssistantView from homeassistant.components.webhook import ( async_generate_id as webhook_generate_id, @@ -35,25 +30,20 @@ from homeassistant.components.webhook import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_TOKEN, CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType -from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER +from .const import DEFAULT_TITLE, DOMAIN, LOGGER from .coordinator import ( WithingsActivityDataUpdateCoordinator, WithingsBedPresenceDataUpdateCoordinator, @@ -66,67 +56,11 @@ from .coordinator import ( PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SENSOR] -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.deprecated(CONF_PROFILES), - cv.deprecated(CONF_CLIENT_ID), - cv.deprecated(CONF_CLIENT_SECRET), - vol.Schema( - { - vol.Optional(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), - vol.Optional(CONF_CLIENT_SECRET): vol.All( - cv.string, vol.Length(min=1) - ), - vol.Optional(CONF_USE_WEBHOOK): cv.boolean, - vol.Optional(CONF_PROFILES): vol.All( - cv.ensure_list, - vol.Unique(), - vol.Length(min=1), - [vol.All(cv.string, vol.Length(min=1))], - ), - } - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) SUBSCRIBE_DELAY = timedelta(seconds=5) UNSUBSCRIBE_DELAY = timedelta(seconds=1) CONF_CLOUDHOOK_URL = "cloudhook_url" -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Withings component.""" - - if conf := config.get(DOMAIN): - async_create_issue( - 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": "Withings", - }, - ) - if CONF_CLIENT_ID in conf: - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - ), - ) - - return True - - @dataclass(slots=True) class WithingsData: """Dataclass to hold withings domain data.""" diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 1b92f23685f..c90455de7ec 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_oauth2_flow -from .const import CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN +from .const import DEFAULT_TITLE, DOMAIN class WithingsFlowHandler( @@ -34,14 +34,8 @@ class WithingsFlowHandler( def extra_authorize_data(self) -> dict[str, str]: """Extra data that needs to be appended to the authorize url.""" return { - "scope": ",".join( - [ - AuthScope.USER_INFO, - AuthScope.USER_METRICS, - AuthScope.USER_ACTIVITY, - AuthScope.USER_SLEEP_EVENTS, - ] - ) + "scope": f"{AuthScope.USER_INFO},{AuthScope.USER_METRICS}," + f"{AuthScope.USER_ACTIVITY},{AuthScope.USER_SLEEP_EVENTS}" } async def async_step_reauth( @@ -71,7 +65,6 @@ class WithingsFlowHandler( return self.async_create_entry( title=DEFAULT_TITLE, data={**data, CONF_WEBHOOK_ID: async_generate_id()}, - options={CONF_USE_WEBHOOK: False}, ) if self.reauth_entry.unique_id == user_id: diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index a87fc8bfe83..91a7b9d9450 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -5,8 +5,6 @@ import logging LOGGER = logging.getLogger(__package__) DEFAULT_TITLE = "Withings" -CONF_PROFILES = "profiles" -CONF_USE_WEBHOOK = "use_webhook" DOMAIN = "withings" diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 19b362dfa0a..0aef11aaa6b 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -45,9 +45,10 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): super().__init__( hass, LOGGER, - name=f"Withings {self.coordinator_name}", + name="", update_interval=self._default_update_interval, ) + self.name = f"Withings {self.config_entry.unique_id} {self.coordinator_name}" self._client = client self.notification_categories: set[NotificationCategory] = set() @@ -63,7 +64,11 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): self, notification_category: NotificationCategory ) -> None: """Update data when webhook is called.""" - LOGGER.debug("Withings webhook triggered for %s", notification_category) + LOGGER.debug( + "Withings webhook triggered for category %s for user %s", + notification_category, + self.config_entry.unique_id, + ) await self.async_request_refresh() async def _async_update_data(self) -> _T: diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 130cc73efd3..79c317f178b 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -94,9 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if bulb.power_monitoring is not False: power: float | None = await bulb.get_power() return power - return None except WIZ_EXCEPTIONS as ex: raise UpdateFailed(f"Failed to update device at {ip_address}: {ex}") from ex + return None coordinator = DataUpdateCoordinator( hass=hass, @@ -113,9 +113,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady as err: + except ConfigEntryNotReady: await bulb.async_close() - raise err + raise async def _async_shutdown_on_stop(event: Event) -> None: await bulb.async_close() diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index 6b51c0fb2cb..88dcce39993 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -1,10 +1,10 @@ { "domain": "wolflink", "name": "Wolf SmartSet Service", - "codeowners": ["@adamkrol93"], + "codeowners": ["@adamkrol93", "@mtielen"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wolflink", "iot_class": "cloud_polling", "loggers": ["wolf_comm"], - "requirements": ["wolf-comm==0.0.6"] + "requirements": ["wolf-comm==0.0.7"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 314f4c6bcf4..e0813cd90cd 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.46"] + "requirements": ["holidays==0.47"] } diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py index 5692ffcb81b..0cf0b557f35 100644 --- a/homeassistant/components/ws66i/config_flow.py +++ b/homeassistant/components/ws66i/config_flow.py @@ -128,12 +128,10 @@ class WS66iConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _key_for_source(index, source, previous_sources): - key = vol.Required( + return vol.Required( source, description={"suggested_value": previous_sources[str(index)]} ) - return key - class Ws66iOptionsFlowHandler(OptionsFlow): """Handle a WS66i options flow.""" diff --git a/homeassistant/components/wyoming/stt.py b/homeassistant/components/wyoming/stt.py index 227fa3a0eca..a28e5fdb527 100644 --- a/homeassistant/components/wyoming/stt.py +++ b/homeassistant/components/wyoming/stt.py @@ -126,8 +126,8 @@ class WyomingSttProvider(stt.SpeechToTextEntity): text = transcript.text break - except (OSError, WyomingError) as err: - _LOGGER.exception("Error processing audio stream: %s", err) + except (OSError, WyomingError): + _LOGGER.exception("Error processing audio stream") return stt.SpeechResult(None, stt.SpeechResultState.ERROR) return stt.SpeechResult( diff --git a/homeassistant/components/wyoming/wake_word.py b/homeassistant/components/wyoming/wake_word.py index 303a87e99bd..6eba0f7ca6d 100644 --- a/homeassistant/components/wyoming/wake_word.py +++ b/homeassistant/components/wyoming/wake_word.py @@ -187,8 +187,8 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): for task in pending: task.cancel() - except (OSError, WyomingError) as err: - _LOGGER.exception("Error processing audio stream: %s", err) + except (OSError, WyomingError): + _LOGGER.exception("Error processing audio stream") return None diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index 90afbe15911..8499864576a 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -61,8 +61,7 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): @property def extra_state_attributes(self) -> dict[str, int]: """Return the state attributes.""" - attributes = {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times} - return attributes + return {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times} @callback def clear_unlock_state(self, _): diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 2f2b705ff60..8ee8bac3fea 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 5384bd93a7e..bea8d9b402f 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -248,7 +248,7 @@ def _async_update_data_vacuum( fan_speeds = device.fan_speed_presets() - data = VacuumCoordinatorData( + return VacuumCoordinatorData( device.status(), device.dnd_status(), device.last_clean_details(), @@ -259,8 +259,6 @@ def _async_update_data_vacuum( {v: k for k, v in fan_speeds.items()}, ) - return data - async def update_async() -> VacuumCoordinatorData: """Fetch data from the device using async_add_executor_job.""" @@ -301,6 +299,7 @@ async def async_create_miio_device_and_coordinator( # List of models requiring specific lazy_discover setting LAZY_DISCOVER_FOR_MODEL = { + "zhimi.fan.za3": True, "zhimi.fan.za5": True, "zhimi.airpurifier.za1": True, } diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 2f5e6e299e9..39cb0ee5f96 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -150,16 +150,15 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) ) - - _LOGGER.debug("Response received from miio device: %s", result) - - return True except DeviceException as exc: if self.available: _LOGGER.error(mask_error, exc) return False + _LOGGER.debug("Response received from miio device: %s", result) + return True + @classmethod def _extract_value_from_attribute(cls, state, attribute): value = getattr(state, attribute) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index cbbf12f9ab1..96f9595e0e8 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -292,10 +292,6 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) ) - - _LOGGER.debug("Response received from light: %s", result) - - return result == SUCCESS except DeviceException as exc: if self._available: _LOGGER.error(mask_error, exc) @@ -303,6 +299,9 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): return False + _LOGGER.debug("Response received from light: %s", result) + return result == SUCCESS + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index c1234b77bbc..5baaf614b01 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -138,8 +138,8 @@ async def async_setup_platform( message = await hass.async_add_executor_job(device.read, slot) _LOGGER.debug("Message received from device: '%s'", message) - if "code" in message and message["code"]: - log_msg = "Received command is: {}".format(message["code"]) + if code := message.get("code"): + log_msg = f"Received command is: {code}" _LOGGER.info(log_msg) persistent_notification.async_create( hass, log_msg, title="Xiaomi Miio Remote" @@ -225,9 +225,9 @@ class XiaomiMiioRemote(RemoteEntity): """Return False if device is unreachable, else True.""" try: self.device.info() - return True except DeviceException: return False + return True async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 02517d00c57..34ebb9addf5 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -805,14 +805,6 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) ) - - _LOGGER.debug("Response received from plug: %s", result) - - # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off. - if func in ["usb_on", "usb_off"] and result == 0: - return True - - return result == SUCCESS except DeviceException as exc: if self._available: _LOGGER.error(mask_error, exc) @@ -820,6 +812,14 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): return False + _LOGGER.debug("Response received from plug: %s", result) + + # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off. + if func in ["usb_on", "usb_off"] and result == 0: + return True + + return result == SUCCESS + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the plug on.""" result = await self._try_command("Turning the plug on failed", self._device.on) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 41f2c2386e1..ef6f94c162f 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -281,10 +281,10 @@ class MiroboVacuum( try: await self.hass.async_add_executor_job(partial(func, *args, **kwargs)) await self.coordinator.async_refresh() - return True except DeviceException as exc: _LOGGER.error(mask_error, exc) return False + return True async def async_start(self) -> None: """Start or resume the cleaning task.""" diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 50797536aee..4f7af2be7ee 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -297,7 +297,7 @@ async def async_send_message( # noqa: C901 _LOGGER.info("Uploading file from URL, %s", filename) - url = await self["xep_0363"].upload_file( + return await self["xep_0363"].upload_file( filename, size=filesize, input_file=result.content, @@ -305,8 +305,6 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - return url - async def upload_file_from_path(self, path, timeout=None): """Upload a file from a local file path via XEP_0363.""" _LOGGER.info("Uploading file from path, %s", path) @@ -328,7 +326,7 @@ async def async_send_message( # noqa: C901 filename = self.get_random_filename(data.get(ATTR_PATH)) _LOGGER.debug("Uploading file with random filename %s", filename) - url = await self["xep_0363"].upload_file( + return await self["xep_0363"].upload_file( filename, size=filesize, input_file=input_file, @@ -336,8 +334,6 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - return url - def send_text_message(self): """Send a text only message to a room or a recipient.""" try: diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index f608da6cd60..8c9c5176003 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -127,9 +127,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) ) return True diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 3edf524c8ad..a068ac6ddca 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -385,7 +385,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): else: children.append(item) - overview = BrowseMedia( + return BrowseMedia( title=media_content_provider.title, media_class=MEDIA_CLASS_MAPPING.get(media_content_provider.content_type), media_content_id=media_content_provider.content_id, @@ -395,8 +395,6 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): children=children, ) - return overview - async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self.coordinator.musiccast.select_sound_mode(self._zone_id, sound_mode) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index cd6759b5864..b7bd1d4784f 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.4.2"] + "requirements": ["yolink-api==0.4.3"] } diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 241b2232eeb..286a6460f19 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -66,9 +66,9 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): value := self.coordinator.data[self.station_id]["TL"]["data"] ) is not None: return float(value) - return None except (KeyError, ValueError, TypeError): return None + return None @property def native_pressure(self) -> float | None: @@ -98,9 +98,9 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): value := self.coordinator.data[self.station_id]["FFX"]["data"] ) is not None: return float(value) - return None except (KeyError, ValueError, TypeError): return None + return None @property def wind_bearing(self) -> float | None: @@ -114,6 +114,6 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): value := self.coordinator.data[self.station_id]["DDX"]["data"] ) is not None: return float(value) - return None except (KeyError, ValueError, TypeError): return None + return None diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 66c41c19474..bbc89e77a76 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -169,9 +169,7 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero # 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.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_zeroconf) hass.data[DOMAIN] = aio_zc return aio_zc @@ -248,9 +246,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) async_when_setup_or_start(hass, "frontend", _async_zeroconf_hass_start) return True diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 42febb3b36d..037ad4192bd 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -224,7 +224,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): default_port = vol.UNDEFINED if self._radio_mgr.device_path is not None: - for description, port in zip(list_of_ports, ports): + for description, port in zip(list_of_ports, ports, strict=False): if port.device == self._radio_mgr.device_path: default_port = description break diff --git a/homeassistant/components/zha/core/cluster_handlers/closures.py b/homeassistant/components/zha/core/cluster_handlers/closures.py index c57ad507317..e96d6492beb 100644 --- a/homeassistant/components/zha/core/cluster_handlers/closures.py +++ b/homeassistant/components/zha/core/cluster_handlers/closures.py @@ -96,8 +96,7 @@ class DoorLockClusterHandler(ClusterHandler): async def async_get_user_code(self, code_slot: int) -> int: """Get the user code from the code slot.""" - result = await self.get_pin_code(code_slot - 1) - return result + return await self.get_pin_code(code_slot - 1) async def async_clear_user_code(self, code_slot: int) -> None: """Clear the code slot.""" @@ -117,8 +116,7 @@ class DoorLockClusterHandler(ClusterHandler): async def async_get_user_type(self, code_slot: int) -> str: """Get user type.""" - result = await self.get_user_type(code_slot - 1) - return result + return await self.get_user_type(code_slot - 1) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Shade.cluster_id) diff --git a/homeassistant/components/zha/core/cluster_handlers/lighting.py b/homeassistant/components/zha/core/cluster_handlers/lighting.py index f19ad311f9e..bde0fdbb0e7 100644 --- a/homeassistant/components/zha/core/cluster_handlers/lighting.py +++ b/homeassistant/components/zha/core/cluster_handlers/lighting.py @@ -2,9 +2,9 @@ from __future__ import annotations -from zigpy.zcl.clusters.lighting import Ballast, Color +from functools import cached_property -from homeassistant.backports.functools import cached_property +from zigpy.zcl.clusters.lighting import Ballast, Color from .. import registries from ..const import REPORT_CONFIG_DEFAULT diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e6d9f3e66b5..e2c725ee529 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta from enum import Enum +from functools import cached_property import logging import random import time @@ -23,7 +24,6 @@ from zigpy.zcl.clusters.general import Groups, Identify from zigpy.zcl.foundation import Status as ZclStatus, ZCLCommandDef import zigpy.zdo.types as zdo_types -from homeassistant.backports.functools import cached_property from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -789,15 +789,6 @@ class ZHADevice(LogMixin): response = await cluster.write_attributes( {attribute: value}, manufacturer=manufacturer ) - self.debug( - "set: %s for attr: %s to cluster: %s for ept: %s - res: %s", - value, - attribute, - cluster_id, - endpoint_id, - response, - ) - return response except zigpy.exceptions.ZigbeeException as exc: raise HomeAssistantError( f"Failed to set attribute: " @@ -807,6 +798,16 @@ class ZHADevice(LogMixin): f"{ATTR_ENDPOINT_ID}: {endpoint_id}" ) from exc + self.debug( + "set: %s for attr: %s to cluster: %s for ept: %s - res: %s", + value, + attribute, + cluster_id, + endpoint_id, + response, + ) + return response + async def issue_cluster_command( self, endpoint_id: int, @@ -995,7 +996,7 @@ class ZHADevice(LogMixin): ) ) res = await asyncio.gather(*(t[0] for t in tasks), return_exceptions=True) - for outcome, log_msg in zip(res, tasks): + for outcome, log_msg in zip(res, tasks, strict=False): if isinstance(outcome, Exception): fmt = f"{log_msg[1]} failed: %s" else: diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index b0d617eb2c2..1bb1750b6ac 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -198,7 +198,7 @@ class Endpoint: gather = functools.partial(gather_with_limited_concurrency, max_concurrency) results = await gather(*tasks, return_exceptions=True) - for cluster_handler, outcome in zip(cluster_handlers, results): + for cluster_handler, outcome in zip(cluster_handlers, results, strict=False): if isinstance(outcome, Exception): cluster_handler.debug( "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 83aa12fbfa1..4c41909f660 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -870,7 +870,10 @@ class LogRelayHandler(logging.Handler): def emit(self, record: LogRecord) -> None: """Relay log message via dispatcher.""" entry = LogEntry( - record, self.paths_re, figure_out_source=record.levelno >= logging.WARNING + record, + self.paths_re, + formatter=self.formatter, + figure_out_source=record.levelno >= logging.WARNING, ) async_dispatcher_send( self.hass, diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 1a001cab381..3f8090f4080 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -14,7 +14,7 @@ from dataclasses import dataclass import enum import logging import re -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, overload +from typing import TYPE_CHECKING, Any, TypeVar, overload import voluptuous as vol import zigpy.exceptions @@ -59,14 +59,10 @@ from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, CUSTOM_CONFIGURATION, DATA from .registries import BINDABLE_CLUSTERS if TYPE_CHECKING: - from .cluster_handlers import ClusterHandler from .device import ZHADevice from .gateway import ZHAGateway -_ClusterHandlerT = TypeVar("_ClusterHandlerT", bound="ClusterHandler") _T = TypeVar("_T") -_R = TypeVar("_R") -_P = ParamSpec("_P") _LOGGER = logging.getLogger(__name__) @@ -102,9 +98,9 @@ async def safe_read( only_cache=only_cache, manufacturer=manufacturer, ) - return result except Exception: # pylint: disable=broad-except return {} + return result async def get_matched_clusters( @@ -296,7 +292,7 @@ def mean_int(*args): def mean_tuple(*args): """Return the mean values along the columns of the supplied values.""" - return tuple(sum(x) / len(x) for x in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args, strict=False)) def reduce_attribute( @@ -508,9 +504,9 @@ def validate_device_class( def validate_device_class( - device_class_enum: type[BinarySensorDeviceClass] - | type[SensorDeviceClass] - | type[NumberDeviceClass], + device_class_enum: type[ + BinarySensorDeviceClass | SensorDeviceClass | NumberDeviceClass + ], metadata_value: enum.Enum, platform: str, logger: logging.Logger, diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 076cb1d420e..8f5a03a7fe5 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -136,8 +136,7 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA) - config = schema(config) - return config + return schema(config) async def async_get_actions( diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index f9f63321d44..f10e377dc46 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -11,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, Event, callback +from homeassistant.core import CALLBACK_TYPE, Event, EventStateChangedData, callback from homeassistant.helpers import entity from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo @@ -19,10 +19,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from .core.const import ( diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 8d65899707e..5e729a74f0d 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -147,11 +147,10 @@ class BaseLight(LogMixin, light.LightEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return state attributes.""" - attributes = { + return { "off_with_transition": self._off_with_transition, "off_brightness": self._off_brightness, } - return attributes @property def is_on(self) -> bool: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7741673557d..b1511b2f5bb 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,12 +21,12 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.38.1", + "bellows==0.38.3", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.114", + "zha-quirks==0.0.115", "zigpy-deconz==0.23.1", - "zigpy==0.63.5", + "zigpy==0.64.0", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py index 5b1f85e1a29..4ee10c7bb93 100644 --- a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py +++ b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py @@ -74,9 +74,14 @@ def _detect_radio_hardware(hass: HomeAssistant, device: str) -> HardwareType: return HardwareType.OTHER -async def probe_silabs_firmware_type(device: str) -> ApplicationType | None: +async def probe_silabs_firmware_type( + device: str, *, probe_methods: ApplicationType | None = None +) -> ApplicationType | None: """Probe the running firmware on a Silabs device.""" - flasher = Flasher(device=device) + flasher = Flasher( + device=device, + **({"probe_methods": probe_methods} if probe_methods else {}), + ) try: await flasher.probe_app_type() diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 91fe302291a..e8507a96e2c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -434,8 +434,7 @@ class Battery(Sensor): # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1 or value == 255: return None - value = round(value / 2) - return value + return round(value / 2) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index f9a92acdd4c..758c3715980 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -1034,7 +1034,7 @@ async def async_binding_operation( ) ) res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True) - for outcome, log_msg in zip(res, bind_tasks): + for outcome, log_msg in zip(res, bind_tasks, strict=False): if isinstance(outcome, Exception): fmt = f"{log_msg[1]} failed: %s" else: diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 2fda501c447..2473200102d 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -29,7 +29,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.helpers import ( collection, config_validation as cv, @@ -166,7 +173,7 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: @callback def _async_add_zone_entity_id( - event_: Event[event.EventStateChangedData], + event_: Event[EventStateChangedData], ) -> None: """Add zone entity ID.""" zone_entity_ids.append(event_.data["entity_id"]) @@ -174,7 +181,7 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: @callback def _async_remove_zone_entity_id( - event_: Event[event.EventStateChangedData], + event_: Event[EventStateChangedData], ) -> None: """Remove zone entity ID.""" zone_entity_ids.remove(event_.data["entity_id"]) @@ -281,9 +288,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated) hass.data[DOMAIN] = storage_collection @@ -315,7 +320,9 @@ async def async_setup_entry( await storage_collection.async_create_item(data) - hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) + hass.async_create_task( + hass.config_entries.async_remove(config_entry.entry_id), eager_start=True + ) return True @@ -331,6 +338,7 @@ class Zone(collection.CollectionEntity): """Representation of a Zone.""" editable: bool + _attr_should_poll = False def __init__(self, config: ConfigType) -> None: """Initialize the zone.""" @@ -339,6 +347,16 @@ class Zone(collection.CollectionEntity): self._attrs: dict | None = None self._remove_listener: Callable[[], None] | None = None self._persons_in_zone: set[str] = set() + self._set_attrs_from_config() + + def _set_attrs_from_config(self) -> None: + """Set the attributes from the config.""" + config = self._config + name: str = config[CONF_NAME] + self._attr_name = name + self._case_folded_name = name.casefold() + self._attr_unique_id = config.get(CONF_ID) + self._attr_icon = config.get(CONF_ICON) @classmethod def from_storage(cls, config: ConfigType) -> Self: @@ -361,46 +379,26 @@ class Zone(collection.CollectionEntity): """Return the state property really does nothing for a zone.""" return len(self._persons_in_zone) - @property - def name(self) -> str: - """Return name.""" - return cast(str, self._config[CONF_NAME]) - - @property - def unique_id(self) -> str | None: - """Return unique ID.""" - return self._config.get(CONF_ID) - - @property - def icon(self) -> str | None: - """Return the icon if any.""" - return self._config.get(CONF_ICON) - - @property - def should_poll(self) -> bool: - """Zone does not poll.""" - return False - async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" if self._config == config: return self._config = config + self._set_attrs_from_config() self._generate_attrs() self.async_write_ha_state() @callback - def _person_state_change_listener( - self, evt: Event[event.EventStateChangedData] - ) -> None: + def _person_state_change_listener(self, evt: Event[EventStateChangedData]) -> None: person_entity_id = evt.data["entity_id"] - cur_count = len(self._persons_in_zone) + persons_in_zone = self._persons_in_zone + cur_count = len(persons_in_zone) if self._state_is_in_zone(evt.data["new_state"]): - self._persons_in_zone.add(person_entity_id) - elif person_entity_id in self._persons_in_zone: - self._persons_in_zone.remove(person_entity_id) + persons_in_zone.add(person_entity_id) + elif person_entity_id in persons_in_zone: + persons_in_zone.remove(person_entity_id) - if len(self._persons_in_zone) != cur_count: + if len(persons_in_zone) != cur_count: self._generate_attrs() self.async_write_ha_state() @@ -408,10 +406,11 @@ class Zone(collection.CollectionEntity): """Run when entity about to be added to hass.""" await super().async_added_to_hass() person_domain = "person" # avoid circular import - persons = self.hass.states.async_entity_ids(person_domain) - for person in persons: - if self._state_is_in_zone(self.hass.states.get(person)): - self._persons_in_zone.add(person) + self._persons_in_zone = { + state.entity_id + for state in self.hass.states.async_all(person_domain) + if self._state_is_in_zone(state) + } self._generate_attrs() self.async_on_remove( @@ -446,7 +445,7 @@ class Zone(collection.CollectionEntity): STATE_UNAVAILABLE, ) and ( - state.state.casefold() == self.name.casefold() + state.state.casefold() == self._case_folded_name or (state.state == STATE_HOME and self.entity_id == ENTITY_ID_HOME) ) ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 8aea25f1f6c..aa4aefe6d95 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -13,17 +13,21 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_ZONE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HassJob, + HomeAssistant, + callback, +) from homeassistant.helpers import ( condition, config_validation as cv, entity_registry as er, location, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 0510ff58d35..e87a2b1531d 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -55,7 +55,7 @@ SET_RUN_STATE_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ZoneMinder component.""" hass.data[DOMAIN] = {} @@ -78,7 +78,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN][host_name] = zm_client try: - success = zm_client.login() and success + success = await hass.async_add_executor_job(zm_client.login) and success except RequestsConnectionError as ex: _LOGGER.error( "ZoneMinder connection failure to %s: %s", @@ -99,7 +99,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: state_name, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA ) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 52c7804bb8f..090a5ecfdf8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -307,7 +307,8 @@ class DriverEvents: controller.on( "node added", lambda event: self.hass.async_create_task( - self.controller_events.async_on_node_added(event["node"]) + self.controller_events.async_on_node_added(event["node"]), + eager_start=False, ), ) ) @@ -415,7 +416,8 @@ class ControllerEvents: node.on( "ready", lambda event: self.hass.async_create_task( - self.node_events.async_on_node_ready(event["node"]) + self.node_events.async_on_node_ready(event["node"]), + eager_start=False, ), ) ) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index ca05dc2117b..3470f64f79f 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -183,7 +183,7 @@ class BaseZwaveJSFlow(ConfigEntryBaseFlow, ABC): @property @abstractmethod - def flow_manager(self) -> FlowManager[ConfigFlowResult, str]: + def flow_manager(self) -> FlowManager[ConfigFlowResult]: """Return the flow manager of the flow.""" async def async_step_install_addon( diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 777d45efddb..3d61699472d 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -151,7 +151,8 @@ async def async_get_device_diagnostics( client: Client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] identifiers = get_home_and_node_id_from_device_entry(device) node_id = identifiers[1] if identifiers else None - assert (driver := client.driver) + driver = client.driver + assert driver if node_id is None or node_id not in driver.controller.nodes: raise ValueError(f"Node for device {device.id} can't be found") node = driver.controller.nodes[node_id] diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 4f1b902d8ba..4a4c1030812 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -96,7 +96,7 @@ def value_matches_matcher( return all( redacted_field_val is None or redacted_field_val == zwave_value_field_val for redacted_field_val, zwave_value_field_val in zip( - astuple(matcher), astuple(zwave_value_id) + astuple(matcher), astuple(zwave_value_id), strict=False ) ) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index a06de5cb8ee..83a139331bb 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.3"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.4"], "usb": [ { "vid": "0658", 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 1005c3bb4db..6b73d1362f9 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 @@ -25,9 +25,7 @@ def get_arguments() -> argparse.Namespace: ), ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def get_fixtures_dir_path(data: dict) -> Path: diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 5567c64ab97..bdd5090bcf8 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -85,7 +85,7 @@ def get_valid_responses_from_results( zwave_objects: Sequence[T], results: Sequence[Any] ) -> Generator[tuple[T, Any], None, None]: """Return valid responses from a list of results.""" - for zwave_object, result in zip(zwave_objects, results): + for zwave_object, result in zip(zwave_objects, results, strict=False): if not isinstance(result, Exception): yield zwave_object, result @@ -96,7 +96,9 @@ def raise_exceptions_from_results( """Raise list of exceptions from a list of results.""" errors: Sequence[tuple[T, Any]] if errors := [ - tup for tup in zip(zwave_objects, results) if isinstance(tup[1], Exception) + tup + for tup in zip(zwave_objects, results, strict=True) + if isinstance(tup[1], Exception) ]: lines = [ *( diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index b3f54ae9904..413186da9bf 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -63,7 +63,8 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): super().__init__(config_entry, driver, info) # Entity class attributes self._attr_available_tones = { - int(id): val for id, val in self.info.primary_value.metadata.states.items() + int(state_id): val + for state_id, val in self.info.primary_value.metadata.states.items() } self._attr_supported_features = ( SirenEntityFeature.TURN_ON diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index 66f02246792..7e00924c221 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -63,8 +63,7 @@ class ZWaveMeController: async def async_establish_connection(self): """Get connection status.""" - is_connected = await self.zwave_api.get_connection() - return is_connected + return await self.zwave_api.get_connection() def add_device(self, device: ZWaveMeData) -> None: """Send signal to create device.""" diff --git a/homeassistant/config.py b/homeassistant/config.py index c570e36c6c1..abb29f6a1a1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -39,6 +39,7 @@ from .const import ( CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, + CONF_DEBUG, CONF_ELEVATION, CONF_EXTERNAL_URL, CONF_ID, @@ -329,8 +330,7 @@ def _validate_currency(data: Any) -> Any: return cv.currency(data) except vol.InInvalid: with suppress(vol.InInvalid): - currency = cv.historic_currency(data) - return currency + return cv.historic_currency(data) raise @@ -392,6 +392,7 @@ CORE_CONFIG_SCHEMA = vol.All( vol.Optional(CONF_CURRENCY): _validate_currency, vol.Optional(CONF_COUNTRY): cv.country, vol.Optional(CONF_LANGUAGE): cv.language, + vol.Optional(CONF_DEBUG): cv.boolean, } ), _filter_bad_internal_external_urls, @@ -464,14 +465,12 @@ def _write_default_config(config_dir: str) -> bool: if not os.path.isfile(scene_yaml_path): with open(scene_yaml_path, "w", encoding="utf8"): pass - - return True - except OSError: print( # noqa: T201 f"Unable to create default configuration file {config_path}" ) return False + return True async def async_hass_config_yaml(hass: HomeAssistant) -> dict: @@ -902,6 +901,9 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non if key in config: setattr(hac, attr, config[key]) + if config.get(CONF_DEBUG): + hac.debug = True + _raise_issue_if_legacy_templates(hass, config.get(CONF_LEGACY_TEMPLATES)) _raise_issue_if_historic_currency(hass, hass.config.currency) _raise_issue_if_no_country(hass, hass.config.country) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 42194641f7f..f982f63b948 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -13,11 +13,11 @@ from collections.abc import ( Mapping, ValuesView, ) -import contextlib from contextvars import ContextVar from copy import deepcopy from enum import Enum, StrEnum import functools +from functools import cached_property import logging from random import randint from types import MappingProxyType @@ -70,8 +70,6 @@ from .util.async_ import create_eager_task from .util.decorator import Registry if TYPE_CHECKING: - from functools import cached_property - from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo @@ -79,8 +77,6 @@ if TYPE_CHECKING: from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo -else: - from .backports.functools import cached_property _LOGGER = logging.getLogger(__name__) @@ -243,7 +239,14 @@ class OperationNotAllowed(ConfigError): UpdateListenerType = Callable[[HomeAssistant, "ConfigEntry"], Coroutine[Any, Any, None]] -FROZEN_CONFIG_ENTRY_ATTRS = {"entry_id", "domain", "state", "reason"} +FROZEN_CONFIG_ENTRY_ATTRS = { + "entry_id", + "domain", + "state", + "reason", + "error_reason_translation_key", + "error_reason_translation_placeholders", +} UPDATE_ENTRY_CONFIG_ENTRY_ATTRS = { "unique_id", "title", @@ -274,10 +277,28 @@ class ConfigEntry: unique_id: str | None state: ConfigEntryState reason: str | None + error_reason_translation_key: str | None + error_reason_translation_placeholders: dict[str, Any] | None pref_disable_new_entities: bool pref_disable_polling: bool version: int + source: str minor_version: int + disabled_by: ConfigEntryDisabler | None + supports_unload: bool | None + supports_remove_device: bool | None + _supports_options: bool | None + _supports_reconfigure: bool | None + update_listeners: list[UpdateListenerType] + _async_cancel_retry_setup: Callable[[], Any] | None + _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None + setup_lock: asyncio.Lock + _reauth_lock: asyncio.Lock + _reconfigure_lock: asyncio.Lock + _tasks: set[asyncio.Future[Any]] + _background_tasks: set[asyncio.Future[Any]] + _integration_for_domain: loader.Integration | None + _tries: int def __init__( self, @@ -329,7 +350,7 @@ class ConfigEntry: _setter(self, "pref_disable_polling", pref_disable_polling) # Source of the configuration (user, discovery, cloud) - self.source = source + _setter(self, "source", source) # State of the entry (LOADED, NOT_LOADED) _setter(self, "state", state) @@ -350,46 +371,46 @@ class ConfigEntry: error_if_core=False, ) disabled_by = ConfigEntryDisabler(disabled_by) - self.disabled_by = disabled_by + _setter(self, "disabled_by", disabled_by) # Supports unload - self.supports_unload: bool | None = None + _setter(self, "supports_unload", None) # Supports remove device - self.supports_remove_device: bool | None = None + _setter(self, "supports_remove_device", None) # Supports options - self._supports_options: bool | None = None + _setter(self, "_supports_options", None) # Supports reconfigure - self._supports_reconfigure: bool | None = None + _setter(self, "_supports_reconfigure", None) # Listeners to call on update - self.update_listeners: list[UpdateListenerType] = [] + _setter(self, "update_listeners", []) # Reason why config entry is in a failed state _setter(self, "reason", None) + _setter(self, "error_reason_translation_key", None) + _setter(self, "error_reason_translation_placeholders", None) # Function to cancel a scheduled retry - self._async_cancel_retry_setup: Callable[[], Any] | None = None + _setter(self, "_async_cancel_retry_setup", None) # Hold list for actions to call on unload. - self._on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None = ( - None - ) + _setter(self, "_on_unload", None) # Reload lock to prevent conflicting reloads - self.reload_lock = asyncio.Lock() + _setter(self, "setup_lock", asyncio.Lock()) # Reauth lock to prevent concurrent reauth flows - self._reauth_lock = asyncio.Lock() + _setter(self, "_reauth_lock", asyncio.Lock()) # Reconfigure lock to prevent concurrent reconfigure flows - self._reconfigure_lock = asyncio.Lock() + _setter(self, "_reconfigure_lock", asyncio.Lock()) - self._tasks: set[asyncio.Future[Any]] = set() - self._background_tasks: set[asyncio.Future[Any]] = set() + _setter(self, "_tasks", set()) + _setter(self, "_background_tasks", set()) - self._integration_for_domain: loader.Integration | None = None - self._tries = 0 + _setter(self, "_integration_for_domain", None) + _setter(self, "_tries", 0) def __repr__(self) -> str: """Representation of ConfigEntry.""" @@ -452,8 +473,7 @@ class ConfigEntry: def clear_cache(self) -> None: """Clear cached properties.""" - with contextlib.suppress(AttributeError): - delattr(self, "as_json_fragment") + self.__dict__.pop("as_json_fragment", None) @cached_property def as_json_fragment(self) -> json_fragment: @@ -472,6 +492,8 @@ class ConfigEntry: "pref_disable_polling": self.pref_disable_polling, "disabled_by": self.disabled_by, "reason": self.reason, + "error_reason_translation_key": self.error_reason_translation_key, + "error_reason_translation_placeholders": self.error_reason_translation_placeholders, } return json_fragment(json_bytes(json_repr)) @@ -543,6 +565,8 @@ class ConfigEntry: setup_phase = SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP error_reason = None + error_reason_translation_key = None + error_reason_translation_placeholders = None try: with async_start_setup( @@ -557,6 +581,8 @@ class ConfigEntry: result = False except ConfigEntryError as exc: error_reason = str(exc) or "Unknown fatal config entry error" + error_reason_translation_key = exc.translation_key + error_reason_translation_placeholders = exc.translation_placeholders _LOGGER.exception( "Error setting up entry %s for %s: %s", self.title, @@ -569,6 +595,8 @@ class ConfigEntry: message = str(exc) auth_base_message = "could not authenticate" error_reason = message or auth_base_message + error_reason_translation_key = exc.translation_key + error_reason_translation_placeholders = exc.translation_placeholders auth_message = ( f"{auth_base_message}: {message}" if message else auth_base_message ) @@ -583,7 +611,15 @@ class ConfigEntry: result = False except ConfigEntryNotReady as exc: message = str(exc) - self._async_set_state(hass, ConfigEntryState.SETUP_RETRY, message or None) + error_reason_translation_key = exc.translation_key + error_reason_translation_placeholders = exc.translation_placeholders + self._async_set_state( + hass, + ConfigEntryState.SETUP_RETRY, + message or None, + error_reason_translation_key, + error_reason_translation_placeholders, + ) wait_time = 2 ** min(self._tries, 4) * 5 + ( randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000 ) @@ -614,7 +650,6 @@ class ConfigEntry: self._async_cancel_retry_setup = hass.bus.async_listen( EVENT_HOMEASSISTANT_STARTED, functools.partial(self._async_setup_again, hass), - run_immediately=True, ) await self._async_process_on_unload(hass) @@ -644,7 +679,13 @@ class ConfigEntry: if result: self._async_set_state(hass, ConfigEntryState.LOADED, None) else: - self._async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason) + self._async_set_state( + hass, + ConfigEntryState.SETUP_ERROR, + error_reason, + error_reason_translation_key, + error_reason_translation_placeholders, + ) @callback def _async_setup_again(self, hass: HomeAssistant, *_: Any) -> None: @@ -657,12 +698,19 @@ 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), + hass.async_create_background_task( + self.async_setup_locked(hass), f"config entry retry {self.domain} {self.title}", eager_start=True, ) + async def async_setup_locked( + self, hass: HomeAssistant, integration: loader.Integration | None = None + ) -> None: + """Set up while holding the setup lock.""" + async with self.setup_lock: + await self.async_setup(hass, integration=integration) + @callback def async_shutdown(self) -> None: """Call when Home Assistant is stopping.""" @@ -730,8 +778,6 @@ class ConfigEntry: self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) await self._async_process_on_unload(hass) - - return result except Exception as exc: # pylint: disable=broad-except _LOGGER.exception( "Error unloading entry %s for %s", self.title, integration.domain @@ -741,6 +787,7 @@ class ConfigEntry: hass, ConfigEntryState.FAILED_UNLOAD, str(exc) or "Unknown error" ) return False + return result async def async_remove(self, hass: HomeAssistant) -> None: """Invoke remove callback on component.""" @@ -771,7 +818,12 @@ class ConfigEntry: @callback def _async_set_state( - self, hass: HomeAssistant, state: ConfigEntryState, reason: str | None + self, + hass: HomeAssistant, + state: ConfigEntryState, + reason: str | None, + error_reason_translation_key: str | None = None, + error_reason_translation_placeholders: dict[str, str] | None = None, ) -> None: """Set the state of the config entry.""" if state not in NO_RESET_TRIES_STATES: @@ -779,6 +831,12 @@ class ConfigEntry: _setter = object.__setattr__ _setter(self, "state", state) _setter(self, "reason", reason) + _setter(self, "error_reason_translation_key", error_reason_translation_key) + _setter( + self, + "error_reason_translation_placeholders", + error_reason_translation_placeholders, + ) self.clear_cache() async_dispatcher_send( hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self @@ -828,12 +886,12 @@ class ConfigEntry: if result: # pylint: disable-next=protected-access hass.config_entries._async_schedule_save() - return result except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error migrating entry %s for %s", self.title, self.domain ) return False + return result def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE: """Listen for when entry is updated. @@ -911,6 +969,7 @@ class ConfigEntry: hass.async_create_task( self._async_init_reauth(hass, context, data), f"config entry reauth {self.title} {self.domain} {self.entry_id}", + eager_start=True, ) async def _async_init_reauth( @@ -1018,7 +1077,7 @@ class ConfigEntry: hass: HomeAssistant, target: Coroutine[Any, Any, _R], name: str | None = None, - eager_start: bool = False, + eager_start: bool = True, ) -> asyncio.Task[_R]: """Create a task from within the event loop. @@ -1026,10 +1085,10 @@ class ConfigEntry: target: target to call. """ - task = hass.async_create_task( + task = hass.async_create_task_internal( target, f"{name} {self.title} {self.domain} {self.entry_id}", eager_start ) - if task.done(): + if eager_start and task.done(): return task self._tasks.add(task) task.add_done_callback(self._tasks.remove) @@ -1042,7 +1101,7 @@ class ConfigEntry: hass: HomeAssistant, target: Coroutine[Any, Any, _R], name: str, - eager_start: bool = False, + eager_start: bool = True, ) -> asyncio.Task[_R]: """Create a background task tied to the config entry lifecycle. @@ -1073,7 +1132,7 @@ class FlowCancelledError(Exception): """Error to indicate that a flow has been cancelled.""" -class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): +class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): """Manage all the config entry flows that are in progress.""" _flow_result = ConfigFlowResult @@ -1096,6 +1155,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str cooldown=DISCOVERY_COOLDOWN, immediate=True, function=self._async_discovery, + background=True, ) async def async_wait_import_flow_initialized(self, handler: str) -> None: @@ -1199,7 +1259,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str async def async_finish_flow( self, - flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + flow: data_entry_flow.FlowHandler[ConfigFlowResult], result: ConfigFlowResult, ) -> ConfigFlowResult: """Finish a config flow and add an entry.""" @@ -1321,7 +1381,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str async def async_post_init( self, - flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + flow: data_entry_flow.FlowHandler[ConfigFlowResult], result: ConfigFlowResult, ) -> None: """After a flow is initialised trigger new flow notifications.""" @@ -1344,7 +1404,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str @callback def _async_discovery(self) -> None: """Handle discovery.""" - self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) + # async_fire_internal is used here because this is only + # called from the Debouncer so we know the usage is safe + self.hass.bus.async_fire_internal(EVENT_FLOW_DISCOVERED) persistent_notification.async_create( self.hass, title="New devices discovered", @@ -1579,7 +1641,7 @@ class ConfigEntries: # starting a new flow with the 'unignore' step. If the integration doesn't # implement async_step_unignore then this will be a no-op. if entry.source == SOURCE_IGNORE: - self.hass.async_create_task( + self.hass.async_create_task_internal( self.hass.config_entries.flow.async_init( entry.domain, context={"source": SOURCE_UNIGNORE}, @@ -1608,9 +1670,7 @@ class ConfigEntries: old_conf_migrate_func=_old_conf_migrator, ) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_shutdown, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) if config is None: self._entries = ConfigEntryItems(self.hass) @@ -1713,12 +1773,31 @@ class ConfigEntries: async def async_reload(self, entry_id: str) -> bool: """Reload an entry. + When reloading from an integration is is preferable to + call async_schedule_reload instead of this method since + it will cancel setup retry before starting this method + in a task which eliminates a race condition where the + setup retry can fire during the reload. + If an entry was not loaded, will just load. """ if (entry := self.async_get_entry(entry_id)) is None: raise UnknownEntry - async with entry.reload_lock: + # Cancel the setup retry task before waiting for the + # reload lock to reduce the chance of concurrent reload + # attempts. + entry.async_cancel_retry_setup() + + if entry.domain not in self.hass.config.components: + # If the component is not loaded, just load it as + # the config entry will be loaded as well. We need + # to do this before holding the lock to avoid a + # deadlock. + await async_setup_component(self.hass, entry.domain, self._hass_config) + return entry.state is ConfigEntryState.LOADED + + async with entry.setup_lock: unload_result = await self.async_unload(entry_id) if not unload_result or entry.disabled_by: @@ -1989,7 +2068,7 @@ def _async_abort_entries_match( raise data_entry_flow.AbortFlow("already_configured") -class ConfigEntryBaseFlow(data_entry_flow.FlowHandler[ConfigFlowResult, str]): +class ConfigEntryBaseFlow(data_entry_flow.FlowHandler[ConfigFlowResult]): """Base class for config and option flows.""" _flow_result = ConfigFlowResult @@ -2327,6 +2406,7 @@ class ConfigFlow(ConfigEntryBaseFlow): data: Mapping[str, Any] | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, reason: str = "reauth_successful", + reload_even_if_entry_is_unchanged: bool = True, ) -> ConfigFlowResult: """Update config entry, reload config entry and finish config flow.""" result = self.hass.config_entries.async_update_entry( @@ -2336,12 +2416,12 @@ class ConfigFlow(ConfigEntryBaseFlow): data=data, options=options, ) - if result: + if reload_even_if_entry_is_unchanged or result: self.hass.config_entries.async_schedule_reload(entry.entry_id) return self.async_abort(reason=reason) -class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): +class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): """Flow to set options for a configuration entry.""" _flow_result = ConfigFlowResult @@ -2371,7 +2451,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): async def async_finish_flow( self, - flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + flow: data_entry_flow.FlowHandler[ConfigFlowResult], result: ConfigFlowResult, ) -> ConfigFlowResult: """Finish an options flow and update options for configuration entry. @@ -2393,7 +2473,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): return result async def _async_setup_preview( - self, flow: data_entry_flow.FlowHandler[ConfigFlowResult, str] + self, flow: data_entry_flow.FlowHandler[ConfigFlowResult] ) -> None: """Set up preview for an option flow handler.""" entry = self._async_get_config_entry(flow.handler) @@ -2469,7 +2549,9 @@ class EntityRegistryDisabledHandler: ) @callback - def _handle_entry_updated(self, event: Event) -> None: + def _handle_entry_updated( + self, event: Event[entity_registry.EventEntityRegistryUpdatedData] + ) -> None: """Handle entity registry entry update.""" if self.registry is None: self.registry = entity_registry.async_get(self.hass) @@ -2507,10 +2589,11 @@ class EntityRegistryDisabledHandler: self._remove_call_later = async_call_later( self.hass, RELOAD_AFTER_UPDATE_DELAY, - HassJob(self._handle_reload, cancel_on_shutdown=True), + HassJob(self._async_handle_reload, cancel_on_shutdown=True), ) - async def _handle_reload(self, _now: Any) -> None: + @callback + def _async_handle_reload(self, _now: Any) -> None: """Handle a reload.""" self._remove_call_later = None to_reload = self.changed @@ -2523,33 +2606,25 @@ class EntityRegistryDisabledHandler: ), ", ".join(to_reload), ) - - await asyncio.gather( - *( - asyncio.create_task( - self.hass.config_entries.async_reload(entry_id), - name="config entry reload {entry.title} {entry.domain} {entry.entry_id}", - ) - for entry_id in to_reload - ) - ) + for entry_id in to_reload: + self.hass.config_entries.async_schedule_reload(entry_id) @callback -def _handle_entry_updated_filter(event_data: Mapping[str, Any]) -> bool: +def _handle_entry_updated_filter( + event_data: entity_registry.EventEntityRegistryUpdatedData, +) -> bool: """Handle entity registry entry update filter. Only handle changes to "disabled_by". If "disabled_by" was CONFIG_ENTRY, reload is not needed. """ - if ( + return not ( 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 - return True + ) async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: diff --git a/homeassistant/const.py b/homeassistant/const.py index 892d16ba008..eb46817bd34 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import StrEnum from functools import partial -from typing import Final +from typing import TYPE_CHECKING, Final from .helpers.deprecation import ( DeprecatedConstant, @@ -13,12 +13,17 @@ from .helpers.deprecation import ( check_if_deprecated_constant, dir_with_deprecated_constants, ) +from .util.event_type import EventType from .util.signal_type import SignalType +if TYPE_CHECKING: + from .core import EventStateChangedData + from .helpers.typing import NoEventData + APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "4" +MINOR_VERSION: Final = 5 +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, 12, 0) @@ -40,6 +45,7 @@ class Platform(StrEnum): CALENDAR = "calendar" CAMERA = "camera" CLIMATE = "climate" + CONVERSATION = "conversation" COVER = "cover" DATE = "date" DATETIME = "datetime" @@ -290,6 +296,7 @@ CONF_WHILE: Final = "while" CONF_WHITELIST: Final = "whitelist" CONF_ALLOWLIST_EXTERNAL_DIRS: Final = "allowlist_external_dirs" LEGACY_CONF_WHITELIST_EXTERNAL_DIRS: Final = "whitelist_external_dirs" +CONF_DEBUG: Final = "debug" CONF_XY: Final = "xy" CONF_ZONE: Final = "zone" @@ -297,16 +304,18 @@ CONF_ZONE: Final = "zone" EVENT_CALL_SERVICE: Final = "call_service" EVENT_COMPONENT_LOADED: Final = "component_loaded" EVENT_CORE_CONFIG_UPDATE: Final = "core_config_updated" -EVENT_HOMEASSISTANT_CLOSE: Final = "homeassistant_close" -EVENT_HOMEASSISTANT_START: Final = "homeassistant_start" -EVENT_HOMEASSISTANT_STARTED: Final = "homeassistant_started" -EVENT_HOMEASSISTANT_STOP: Final = "homeassistant_stop" -EVENT_HOMEASSISTANT_FINAL_WRITE: Final = "homeassistant_final_write" +EVENT_HOMEASSISTANT_CLOSE: EventType[NoEventData] = EventType("homeassistant_close") +EVENT_HOMEASSISTANT_START: EventType[NoEventData] = EventType("homeassistant_start") +EVENT_HOMEASSISTANT_STARTED: EventType[NoEventData] = EventType("homeassistant_started") +EVENT_HOMEASSISTANT_STOP: EventType[NoEventData] = EventType("homeassistant_stop") +EVENT_HOMEASSISTANT_FINAL_WRITE: EventType[NoEventData] = EventType( + "homeassistant_final_write" +) EVENT_LOGBOOK_ENTRY: Final = "logbook_entry" 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_CHANGED: EventType[EventStateChangedData] = EventType("state_changed") EVENT_STATE_REPORTED: Final = "state_reported" EVENT_THEMES_UPDATED: Final = "themes_updated" EVENT_PANELS_UPDATED: Final = "panels_updated" diff --git a/homeassistant/core.py b/homeassistant/core.py index d4510e970f9..2b1b9756a50 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -23,6 +23,7 @@ from dataclasses import dataclass import datetime import enum import functools +from functools import cached_property import inspect import logging import os @@ -35,7 +36,6 @@ from typing import ( TYPE_CHECKING, Any, Generic, - Literal, NotRequired, ParamSpec, Self, @@ -50,7 +50,7 @@ from typing_extensions import TypeVar import voluptuous as vol import yarl -from . import block_async_io, util +from . import util from .const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, @@ -85,6 +85,7 @@ from .exceptions import ( InvalidStateError, MaxLengthExceeded, ServiceNotFound, + ServiceValidationError, Unauthorized, ) from .helpers.deprecation import ( @@ -101,6 +102,7 @@ from .util.async_ import ( run_callback_threadsafe, shutdown_run_callback_threadsafe, ) +from .util.event_type import EventType from .util.executor import InterruptibleThreadPoolExecutor from .util.json import JsonObjectType from .util.read_only_dict import ReadOnlyDict @@ -116,21 +118,16 @@ from .util.unit_system import ( # Typing imports that create a circular dependency if TYPE_CHECKING: - from functools import cached_property - from .auth import AuthManager from .components.http import ApiConfig, HomeAssistantHTTP from .config_entries import ConfigEntries from .helpers.entity import StateInfo -else: - from .backports.functools import cached_property STOPPING_STAGE_SHUTDOWN_TIMEOUT = 20 STOP_STAGE_SHUTDOWN_TIMEOUT = 100 FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT = 60 CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30 -block_async_io.enable() _T = TypeVar("_T") _R = TypeVar("_R") @@ -139,6 +136,7 @@ _P = ParamSpec("_P") _Ts = TypeVarTuple("_Ts") # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} +_SENTINEL = object() _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) _DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) CALLBACK_TYPE = Callable[[], None] @@ -165,6 +163,14 @@ class ConfigSource(enum.StrEnum): YAML = "yaml" +class EventStateChangedData(TypedDict): + """EventStateChanged data.""" + + entity_id: str + old_state: State | None + new_state: State | None + + # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead _DEPRECATED_SOURCE_DISCOVERED = DeprecatedConstantEnum( ConfigSource.DISCOVERED, "2025.1" @@ -272,17 +278,24 @@ def async_get_hass() -> HomeAssistant: return _hass.hass +class ReleaseChannel(enum.StrEnum): + BETA = "beta" + DEV = "dev" + NIGHTLY = "nightly" + STABLE = "stable" + + @callback -def get_release_channel() -> Literal["beta", "dev", "nightly", "stable"]: +def get_release_channel() -> ReleaseChannel: """Find release channel based on version number.""" version = __version__ if "dev0" in version: - return "dev" + return ReleaseChannel.DEV if "dev" in version: - return "nightly" + return ReleaseChannel.NIGHTLY if "b" in version: - return "beta" - return "stable" + return ReleaseChannel.BETA + return ReleaseChannel.STABLE @enum.unique @@ -377,7 +390,7 @@ class HomeAssistant: http: HomeAssistantHTTP = None # type: ignore[assignment] config_entries: ConfigEntries = None # type: ignore[assignment] - def __new__(cls, config_dir: str) -> HomeAssistant: + def __new__(cls, config_dir: str) -> Self: """Set the _hass thread local data.""" hass = super().__new__(cls) _hass.hass = hass @@ -416,6 +429,20 @@ class HomeAssistant: max_workers=1, thread_name_prefix="ImportExecutor" ) + def verify_event_loop_thread(self, what: str) -> None: + """Report and raise if we are not running in the event loop thread.""" + if ( + loop_thread_ident := self.loop.__dict__.get("_thread_ident") + ) and loop_thread_ident != threading.get_ident(): + from .helpers import frame # pylint: disable=import-outside-toplevel + + # frame is a circular import, so we import it here + frame.report( + f"calls {what} from a thread", + error_if_core=True, + error_if_integration=True, + ) + @property def _active_tasks(self) -> set[asyncio.Future[Any]]: """Return all active tasks. @@ -442,8 +469,7 @@ class HomeAssistant: """Set the current state.""" self.state = state for prop in ("is_running", "is_stopping"): - with suppress(AttributeError): - delattr(self, prop) + self.__dict__.pop(prop, None) def start(self) -> int: """Start Home Assistant. @@ -491,11 +517,10 @@ class HomeAssistant: This method is a coroutine. """ _LOGGER.info("Starting Home Assistant") - setattr(self.loop, "_thread_ident", threading.get_ident()) self.set_state(CoreState.starting) - self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) - self.bus.async_fire(EVENT_HOMEASSISTANT_START) + self.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_START) if not self._tasks: pending: set[asyncio.Future[Any]] | None = None @@ -528,8 +553,8 @@ class HomeAssistant: return self.set_state(CoreState.running) - self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) - self.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + self.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_STARTED) def add_job( self, target: Callable[[*_Ts], Any] | Coroutine[Any, Any, Any], *args: *_Ts @@ -553,7 +578,7 @@ class HomeAssistant: target = cast(Callable[[*_Ts], Any], target) self.loop.call_soon_threadsafe( functools.partial( - self.async_add_hass_job, HassJob(target), *args, eager_start=True + self._async_add_hass_job, HassJob(target), *args, eager_start=True ) ) @@ -625,7 +650,7 @@ class HomeAssistant: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: target = cast(Callable[[*_Ts], Coroutine[Any, Any, _R] | _R], target) - return self.async_add_hass_job(HassJob(target), *args, eager_start=eager_start) + return self._async_add_hass_job(HassJob(target), *args, eager_start=eager_start) @overload @callback @@ -660,6 +685,58 @@ class HomeAssistant: 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. + """ + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_add_hass_job`, which is deprecated and will be removed in Home " + "Assistant 2025.5; Please review " + "https://developers.home-assistant.io/blog/2024/04/07/deprecate_add_hass_job" + " for replacement options", + error_if_core=False, + ) + + return self._async_add_hass_job( + hassjob, *args, eager_start=eager_start, background=background + ) + + @overload + @callback + def _async_add_hass_job( + 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, + 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, + 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. @@ -708,7 +785,9 @@ class HomeAssistant: target: target to call. """ self.loop.call_soon_threadsafe( - functools.partial(self.async_create_task, target, name, eager_start=True) + functools.partial( + self.async_create_task_internal, target, name, eager_start=True + ) ) @callback @@ -716,13 +795,44 @@ class HomeAssistant: self, target: Coroutine[Any, Any, _R], name: str | None = None, - eager_start: bool = False, + eager_start: bool = True, ) -> asyncio.Task[_R]: """Create a task from within the event loop. This method must be run in the event loop. If you are using this in your integration, use the create task methods on the config entry instead. + target: target to call. + """ + # We turned on asyncio debug in April 2024 in the dev containers + # in the hope of catching some of the issues that have been + # reported. It will take a while to get all the issues fixed in + # custom components. + # + # In 2025.5 we should guard the `verify_event_loop_thread` + # check with a check for the `hass.config.debug` flag being set as + # long term we don't want to be checking this in production + # environments since it is a performance hit. + self.verify_event_loop_thread("async_create_task") + return self.async_create_task_internal(target, name, eager_start) + + @callback + def async_create_task_internal( + self, + target: Coroutine[Any, Any, _R], + name: str | None = None, + eager_start: bool = True, + ) -> asyncio.Task[_R]: + """Create a task from within the event loop, internal use only. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking change to this function in the future and it + should not be used in integrations. + + This method must be run in the event loop. If you are using this in your + integration, use the create task methods on the config entry instead. + target: target to call. """ if eager_start: @@ -739,7 +849,7 @@ class HomeAssistant: @callback def async_create_background_task( - self, target: Coroutine[Any, Any, _R], name: str, eager_start: bool = False + self, target: Coroutine[Any, Any, _R], name: str, eager_start: bool = True ) -> asyncio.Task[_R]: """Create a task from within the event loop. @@ -837,7 +947,7 @@ class HomeAssistant: hassjob.target(*args) return None - return self.async_add_hass_job( + return self._async_add_hass_job( hassjob, *args, eager_start=True, background=background ) @@ -1051,7 +1161,7 @@ class HomeAssistant: self.exit_code = exit_code self.set_state(CoreState.stopping) - self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_STOP) try: async with self.timeout.async_timeout(STOP_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() @@ -1064,7 +1174,7 @@ class HomeAssistant: # Stage 3 - Final write self.set_state(CoreState.final_write) - self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_FINAL_WRITE) try: async with self.timeout.async_timeout(FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() @@ -1077,7 +1187,7 @@ class HomeAssistant: # Stage 4 - Close self.set_state(CoreState.not_running) - self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_CLOSE) # Make a copy of running_tasks since a task can finish # while we are awaiting canceled tasks to get their result @@ -1105,10 +1215,8 @@ class HomeAssistant: _LOGGER.exception( "Task %s could not be canceled during final shutdown stage", task ) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception( - "Task %s error during final shutdown stage: %s", task, exc - ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Task %s error during final shutdown stage", task) # Prevent run_callback_threadsafe from scheduling any additional # callbacks in the event loop as callbacks created on the futures @@ -1167,9 +1275,9 @@ class Context: self.parent_id = parent_id self.origin_event: Event[Any] | None = None - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Compare contexts.""" - return bool(self.__class__ == other.__class__ and self.id == other.id) + return isinstance(other, Context) and self.id == other.id @cached_property def _as_dict(self) -> dict[str, str | None]: @@ -1215,7 +1323,7 @@ class Event(Generic[_DataT]): def __init__( self, - event_type: str, + event_type: EventType[_DataT] | str, data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, time_fired_timestamp: float | None = None, @@ -1289,7 +1397,7 @@ class Event(Generic[_DataT]): def _event_repr( - event_type: str, origin: EventOrigin, data: Mapping[str, Any] | None + event_type: EventType[_DataT] | str, origin: EventOrigin, data: _DataT | None ) -> str: """Return the representation.""" if data: @@ -1301,18 +1409,17 @@ def _event_repr( _FilterableJobType = tuple[ HassJob[[Event[_DataT]], Coroutine[Any, Any, None] | None], # job Callable[[_DataT], bool] | None, # event_filter - bool, # run_immediately ] @dataclass(slots=True) -class _OneTimeListener: +class _OneTimeListener(Generic[_DataT]): hass: HomeAssistant - listener_job: HassJob[[Event], Coroutine[Any, Any, None] | None] + listener_job: HassJob[[Event[_DataT]], Coroutine[Any, Any, None] | None] remove: CALLBACK_TYPE | None = None @callback - def __call__(self, event: Event) -> None: + def __call__(self, event: Event[_DataT]) -> None: """Remove listener from event bus and then fire listener.""" if not self.remove: # If the listener was already removed, we don't need to do anything @@ -1329,10 +1436,16 @@ class _OneTimeListener: return f"<_OneTimeListener {self.listener_job.target}>" -# Empty list, used by EventBus._async_fire +# Empty list, used by EventBus.async_fire_internal EMPTY_LIST: list[Any] = [] +def _verify_event_type_length_or_raise(event_type: EventType[_DataT] | str) -> None: + """Verify the length of the event type and raise if too long.""" + if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: + raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE) + + class EventBus: """Allow the firing of and listening for events.""" @@ -1340,14 +1453,12 @@ class EventBus: def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners: dict[str, list[_FilterableJobType[Any]]] = {} + self._listeners: dict[EventType[Any] | 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 - ) + self.async_listen(EVENT_LOGGING_CHANGED, self._async_logging_changed) @callback def _async_logging_changed(self, event: Event | None = None) -> None: @@ -1355,7 +1466,7 @@ class EventBus: self._debug = _LOGGER.isEnabledFor(logging.DEBUG) @callback - def async_listeners(self) -> dict[str, int]: + def async_listeners(self) -> dict[EventType[Any] | str, int]: """Return dictionary with events and the number of listeners. This method must be run in the event loop. @@ -1363,27 +1474,28 @@ class EventBus: return {key: len(listeners) for key, listeners in self._listeners.items()} @property - def listeners(self) -> dict[str, int]: + def listeners(self) -> dict[EventType[Any] | str, int]: """Return dictionary with events and the number of listeners.""" return run_callback_threadsafe(self._hass.loop, self.async_listeners).result() def fire( self, - event_type: str, - event_data: Mapping[str, Any] | None = None, + event_type: EventType[_DataT] | str, + event_data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, ) -> None: """Fire an event.""" + _verify_event_type_length_or_raise(event_type) self._hass.loop.call_soon_threadsafe( - self.async_fire, event_type, event_data, origin, context + self.async_fire_internal, event_type, event_data, origin, context ) @callback def async_fire( self, - event_type: str, - event_data: Mapping[str, Any] | None = None, + event_type: EventType[_DataT] | str, + event_data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, time_fired: float | None = None, @@ -1392,22 +1504,27 @@ class EventBus: This method must be run in the event loop. """ - if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: - raise MaxLengthExceeded( - event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE - ) - return self._async_fire(event_type, event_data, origin, context, time_fired) + _verify_event_type_length_or_raise(event_type) + self._hass.verify_event_loop_thread("async_fire") + return self.async_fire_internal( + event_type, event_data, origin, context, time_fired + ) @callback - def _async_fire( + def async_fire_internal( self, - event_type: str, - event_data: Mapping[str, Any] | None = None, + event_type: EventType[_DataT] | str, + event_data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, time_fired: float | None = None, ) -> None: - """Fire an event. + """Fire an event, for internal use only. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking change to this function in the future and it + should not be used in integrations. This method must be run in the event loop. """ @@ -1430,9 +1547,9 @@ class EventBus: if not listeners: return - event: Event | None = None + event: Event[_DataT] | None = None - for job, event_filter, run_immediately in listeners: + for job, event_filter in listeners: if event_filter is not None: try: if event_data is None or not event_filter(event_data): @@ -1450,18 +1567,15 @@ class EventBus: context, ) - if run_immediately: - try: - self._hass.async_run_hass_job(job, event) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error running job: %s", job) - else: - self._hass.async_add_hass_job(job, event) + try: + self._hass.async_run_hass_job(job, event) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error running job: %s", job) def listen( self, - event_type: str, - listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], + event_type: EventType[_DataT] | str, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1481,10 +1595,10 @@ class EventBus: @callback def async_listen( self, - event_type: str, + event_type: EventType[_DataT] | str, listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], event_filter: Callable[[_DataT], bool] | None = None, - run_immediately: bool = False, + run_immediately: bool | object = _SENTINEL, ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1501,6 +1615,16 @@ class EventBus: This method must be run in the event loop. """ + if run_immediately in (True, False): + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_listen` with run_immediately, which is" + " deprecated and will be removed in Home Assistant 2025.5", + error_if_core=False, + ) + 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 event_type == EVENT_STATE_REPORTED: @@ -1508,22 +1632,19 @@ class EventBus: 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}"), event_filter, - run_immediately, ), ) @callback def _async_listen_filterable_job( - self, event_type: str, filterable_job: _FilterableJobType[Any] + self, + event_type: EventType[_DataT] | str, + filterable_job: _FilterableJobType[_DataT], ) -> CALLBACK_TYPE: self._listeners.setdefault(event_type, []).append(filterable_job) return functools.partial( @@ -1532,8 +1653,8 @@ class EventBus: def listen_once( self, - event_type: str, - listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], + event_type: EventType[_DataT] | str, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1555,9 +1676,9 @@ class EventBus: @callback def async_listen_once( self, - event_type: str, - listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], - run_immediately: bool = False, + event_type: EventType[_DataT] | str, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], + run_immediately: bool | object = _SENTINEL, ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1568,7 +1689,19 @@ class EventBus: This method must be run in the event loop. """ - one_time_listener = _OneTimeListener(self._hass, HassJob(listener)) + if run_immediately in (True, False): + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_listen_once` with run_immediately, which is " + "deprecated and will be removed in Home Assistant 2025.5", + error_if_core=False, + ) + + one_time_listener: _OneTimeListener[_DataT] = _OneTimeListener( + self._hass, HassJob(listener) + ) remove = self._async_listen_filterable_job( event_type, ( @@ -1578,7 +1711,6 @@ class EventBus: job_type=HassJobType.Callback, ), None, - run_immediately, ), ) one_time_listener.remove = remove @@ -1586,7 +1718,9 @@ class EventBus: @callback def _async_remove_listener( - self, event_type: str, filterable_job: _FilterableJobType + self, + event_type: EventType[_DataT] | str, + filterable_job: _FilterableJobType[_DataT], ) -> None: """Remove a listener of a specific event_type. @@ -1679,11 +1813,15 @@ class State: @cached_property def last_changed_timestamp(self) -> float: """Timestamp of last change.""" + if self.last_changed == self.last_updated: + return self.last_updated_timestamp return self.last_changed.timestamp() @cached_property def last_reported_timestamp(self) -> float: """Timestamp of last report.""" + if self.last_reported == self.last_updated: + return self.last_updated_timestamp return self.last_reported.timestamp() @cached_property @@ -2027,9 +2165,14 @@ class StateMachine: return False old_state.expire() - self._bus._async_fire( # pylint: disable=protected-access + state_changed_data: EventStateChangedData = { + "entity_id": entity_id, + "old_state": old_state, + "new_state": None, + } + self._bus.async_fire_internal( EVENT_STATE_CHANGED, - {"entity_id": entity_id, "old_state": old_state, "new_state": None}, + state_changed_data, context=context, ) return True @@ -2140,7 +2283,7 @@ class StateMachine: # 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 + self._bus.async_fire_internal( EVENT_STATE_REPORTED, { "entity_id": entity_id, @@ -2178,9 +2321,14 @@ class StateMachine: if old_state is not None: old_state.expire() self._states[entity_id] = state - self._bus._async_fire( # pylint: disable=protected-access + state_changed_data: EventStateChangedData = { + "entity_id": entity_id, + "old_state": old_state, + "new_state": state, + } + self._bus.async_fire_internal( EVENT_STATE_CHANGED, - {"entity_id": entity_id, "old_state": old_state, "new_state": state}, + state_changed_data, context=context, time_fired=timestamp, ) @@ -2341,7 +2489,7 @@ class ServiceRegistry: """ run_callback_threadsafe( self._hass.loop, - self.async_register, + self._async_register, domain, service, service_func, @@ -2369,6 +2517,33 @@ class ServiceRegistry: Schema is called to coerce and validate the service data. + This method must be run in the event loop. + """ + self._hass.verify_event_loop_thread("async_register") + self._async_register( + domain, service, service_func, schema, supports_response, job_type + ) + + @callback + def _async_register( + self, + domain: str, + service: str, + service_func: Callable[ + [ServiceCall], + Coroutine[Any, Any, ServiceResponse | EntityServiceResponse] + | ServiceResponse + | EntityServiceResponse + | None, + ], + schema: vol.Schema | None = None, + supports_response: SupportsResponse = SupportsResponse.NONE, + job_type: HassJobType | None = None, + ) -> None: + """Register a service. + + Schema is called to coerce and validate the service data. + This method must be run in the event loop. """ domain = domain.lower() @@ -2387,20 +2562,29 @@ class ServiceRegistry: else: self._services[domain] = {service: service_obj} - self._hass.bus.async_fire( + self._hass.bus.async_fire_internal( EVENT_SERVICE_REGISTERED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) def remove(self, domain: str, service: str) -> None: """Remove a registered service from service handler.""" run_callback_threadsafe( - self._hass.loop, self.async_remove, domain, service + self._hass.loop, self._async_remove, domain, service ).result() @callback def async_remove(self, domain: str, service: str) -> None: """Remove a registered service from service handler. + This method must be run in the event loop. + """ + self._hass.verify_event_loop_thread("async_remove") + self._async_remove(domain, service) + + @callback + def _async_remove(self, domain: str, service: str) -> None: + """Remove a registered service from service handler. + This method must be run in the event loop. """ domain = domain.lower() @@ -2415,7 +2599,7 @@ class ServiceRegistry: if not self._services[domain]: self._services.pop(domain) - self._hass.bus.async_fire( + self._hass.bus.async_fire_internal( EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) @@ -2488,16 +2672,27 @@ class ServiceRegistry: if return_response: if not blocking: - raise ValueError( - "Invalid argument return_response=True when blocking=False" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_should_be_blocking", + translation_placeholders={ + "return_response": "return_response=True", + "non_blocking_argument": "blocking=False", + }, ) if handler.supports_response is SupportsResponse.NONE: - raise ValueError( - "Invalid argument return_response=True when handler does not support responses" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_does_not_support_response", + translation_placeholders={ + "return_response": "return_response=True" + }, ) elif handler.supports_response is SupportsResponse.ONLY: - raise ValueError( - "Service call requires responses but caller did not ask for responses" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_lacks_response_request", + translation_placeholders={"return_response": "return_response=True"}, ) if target: @@ -2521,7 +2716,7 @@ class ServiceRegistry: domain, service, processed_data, context, return_response ) - self._hass.bus._async_fire( # pylint: disable=protected-access + self._hass.bus.async_fire_internal( EVENT_CALL_SERVICE, { ATTR_DOMAIN: domain, @@ -2533,7 +2728,7 @@ class ServiceRegistry: coro = self._execute_service(handler, service_call) if not blocking: - self._hass.async_create_task( + self._hass.async_create_task_internal( self._run_service_call_catch_exceptions(coro, service_call), f"service call background {service_call.domain}.{service_call.service}", eager_start=True, @@ -2545,7 +2740,11 @@ class ServiceRegistry: return None if not isinstance(response_data, dict): raise HomeAssistantError( - f"Service response data expected a dictionary, was {type(response_data)}" + translation_domain=DOMAIN, + translation_key="service_reponse_invalid", + translation_placeholders={ + "response_data_type": str(type(response_data)) + }, ) return response_data @@ -2587,6 +2786,41 @@ class ServiceRegistry: return await self._hass.async_add_executor_job(target, service_call) +class _ComponentSet(set[str]): + """Set of loaded components. + + This set contains both top level components and platforms. + + Examples: + `light`, `switch`, `hue`, `mjpeg.camera`, `universal.media_player`, + `homeassistant.scene` + + The top level components set only contains the top level components. + + """ + + def __init__(self, top_level_components: set[str]) -> None: + """Initialize the component set.""" + self._top_level_components = top_level_components + + def add(self, component: str) -> None: + """Add a component to the store.""" + if "." not in component: + self._top_level_components.add(component) + return super().add(component) + + def remove(self, component: str) -> None: + """Remove a component from the store.""" + if "." in component: + raise ValueError("_ComponentSet does not support removing sub-components") + self._top_level_components.remove(component) + return super().remove(component) + + def discard(self, component: str) -> None: + """Remove a component from the store.""" + raise NotImplementedError("_ComponentSet does not support discard, use remove") + + class Config: """Configuration settings for Home Assistant.""" @@ -2602,6 +2836,7 @@ class Config: self.elevation: int = 0 """Elevation (always in meters regardless of the unit system).""" + self.debug: bool = False self.location_name: str = "Home" self.time_zone: str = "UTC" self.units: UnitSystem = METRIC_SYSTEM @@ -2619,8 +2854,13 @@ class Config: # List of packages to skip when installing requirements on startup self.skip_pip_packages: list[str] = [] - # List of loaded components - self.components: set[str] = set() + # Set of loaded top level components + # This set is updated by _ComponentSet + # and should not be modified directly + self.top_level_components: set[str] = set() + + # Set of loaded components + self.components: _ComponentSet = _ComponentSet(self.top_level_components) # API (HTTP) server configuration self.api: ApiConfig | None = None @@ -2701,9 +2941,10 @@ class Config: for allowed_path in self.allowlist_external_dirs: try: thepath.relative_to(allowed_path) - return True except ValueError: pass + else: + return True return False @@ -2736,6 +2977,7 @@ class Config: "country": self.country, "language": self.language, "safe_mode": self.safe_mode, + "debug": self.debug, } def set_time_zone(self, time_zone_str: str) -> None: @@ -2802,7 +3044,7 @@ class Config: self._update(source=ConfigSource.STORAGE, **kwargs) await self._async_store() - self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs) + self.hass.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE, kwargs) _raise_issue_if_historic_currency(self.hass, self.currency) _raise_issue_if_no_country(self.hass, self.country) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 6a1453c9ff3..f628879a7fd 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import asyncio -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Container, Iterable, Mapping from contextlib import suppress import copy from dataclasses import dataclass @@ -153,7 +153,7 @@ class FlowResult(TypedDict, Generic[_HandlerT], total=False): flow_id: Required[str] handler: Required[_HandlerT] last_step: bool | None - menu_options: list[str] | dict[str, str] + menu_options: Container[str] options: Mapping[str, Any] preview: str | None progress_action: str @@ -442,7 +442,7 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): ) ): # Tell frontend to reload the flow state. - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_DATA_ENTRY_FLOW_PROGRESSED, {"handler": flow.handler, "flow_id": flow_id, "refresh": True}, ) @@ -489,8 +489,8 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): flow.async_cancel_progress_task() try: flow.async_remove() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error removing %s flow: %s", flow.handler, err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error removing %s flow", flow.handler) async def _async_handle_step( self, @@ -843,7 +843,7 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]): self, *, step_id: str | None = None, - menu_options: list[str] | dict[str, str], + menu_options: Container[str], description_placeholders: Mapping[str, str] | None = None, ) -> _FlowResultT: """Show a navigation menu to the user. diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index bdf4d8c060b..044a41aab7a 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -4,7 +4,9 @@ from __future__ import annotations from collections.abc import Callable, Generator, Sequence from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any + +from .util.event_type import EventType if TYPE_CHECKING: from .core import Context @@ -253,7 +255,7 @@ class UnknownUser(Unauthorized): """When call is made with user ID that doesn't exist.""" -class ServiceNotFound(HomeAssistantError): +class ServiceNotFound(ServiceValidationError): """Raised when a service is not found.""" def __init__(self, domain: str, service: str) -> None: @@ -271,8 +273,12 @@ class ServiceNotFound(HomeAssistantError): class MaxLengthExceeded(HomeAssistantError): """Raised when a property value has exceeded the max character length.""" - def __init__(self, value: str, property_name: str, max_length: int) -> None: + def __init__( + self, value: EventType[Any] | str, property_name: str, max_length: int + ) -> None: """Initialize error.""" + if TYPE_CHECKING: + value = str(value) super().__init__( translation_domain="homeassistant", translation_key="max_length_exceeded", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index cd8174bab1f..3c18c27057a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -66,6 +66,21 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "dormakaba_dkey", "service_uuid": "e7a60001-6639-429f-94fd-86de8ea26897", }, + { + "connectable": True, + "domain": "eq3btsmart", + "local_name": "CC-RT-BLE", + }, + { + "connectable": True, + "domain": "eq3btsmart", + "local_name": "CC-RT-M-BLE", + }, + { + "connectable": True, + "domain": "eq3btsmart", + "local_name": "CC-RT-BLE-EQ", + }, { "domain": "eufylife_ble", "local_name": "eufy T9140", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8d46c8be240..6f6ce237904 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,6 +42,7 @@ FLOWS = { "alarmdecoder", "amberelectric", "ambiclimate", + "ambient_network", "ambient_station", "analytics_insights", "android_ip_webcam", @@ -55,6 +56,7 @@ FLOWS = { "aprilaire", "aranet", "arcam_fmj", + "arve", "aseko_pool_live", "asuswrt", "atag", @@ -144,12 +146,16 @@ FLOWS = { "elvia", "emonitor", "emulated_roku", + "energenie_power_sockets", "energyzero", + "enigma2", "enocean", "enphase_envoy", "environment_canada", + "epic_games_store", "epion", "epson", + "eq3btsmart", "escea", "esphome", "eufylife_ble", @@ -169,6 +175,7 @@ FLOWS = { "flo", "flume", "flux_led", + "folder_watcher", "forecast_solar", "forked_daapd", "foscam", @@ -282,6 +289,7 @@ FLOWS = { "ld2410_ble", "leaone", "led_ble", + "lg_netcast", "lg_soundbar", "lidarr", "lifx", @@ -452,6 +460,7 @@ FLOWS = { "rympro", "sabnzbd", "samsungtv", + "sanix", "schlage", "scrape", "screenlogic", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4f9f822e85e..9c5d25a7f22 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -58,6 +58,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "axis-b8a44f*", "macaddress": "B8A44F*", }, + { + "domain": "axis", + "hostname": "axis-e82725*", + "macaddress": "E82725*", + }, { "domain": "blink", "hostname": "blink*", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b8abac5145b..e6a103989d1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -244,6 +244,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "ambient_network": { + "name": "Ambient Weather Network", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "ambient_station": { "name": "Ambient Weather Station", "integration_type": "hub", @@ -271,7 +277,8 @@ "name": "Home Assistant Analytics Insights", "integration_type": "service", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "android_ip_webcam": { "name": "Android IP Webcam", @@ -454,6 +461,12 @@ } } }, + "arve": { + "name": "Arve", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "arwn": { "name": "Ambient Radio Weather Network", "integration_type": "hub", @@ -950,7 +963,8 @@ "color_extractor": { "name": "ColorExtractor", "integration_type": "hub", - "config_flow": true + "config_flow": true, + "single_config_entry": true }, "comed": { "name": "Commonwealth Edison (ComEd)", @@ -1298,7 +1312,8 @@ "downloader": { "name": "Downloader", "integration_type": "hub", - "config_flow": true + "config_flow": true, + "single_config_entry": true }, "dremel_3d_printer": { "name": "Dremel 3D Printer", @@ -1571,6 +1586,11 @@ "config_flow": true, "iot_class": "local_push" }, + "energenie_power_sockets": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "energie_vanons": { "name": "Energie VanOns", "integration_type": "virtual", @@ -1584,8 +1604,8 @@ }, "enigma2": { "name": "Enigma2 (OpenWebif)", - "integration_type": "hub", - "config_flow": false, + "integration_type": "device", + "config_flow": true, "iot_class": "local_polling" }, "enmax": { @@ -1629,6 +1649,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "epic_games_store": { + "name": "Epic Games Store", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "epion": { "name": "Epion", "integration_type": "hub", @@ -1637,20 +1663,9 @@ }, "epson": { "name": "Epson", - "integrations": { - "epson": { - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_polling", - "name": "Epson" - }, - "epsonworkforce": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling", - "name": "Epson Workforce" - } - } + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" }, "eq3": { "name": "eQ-3", @@ -1660,6 +1675,12 @@ "config_flow": false, "iot_class": "local_polling", "name": "eQ-3 MAX!" + }, + "eq3btsmart": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling", + "name": "eQ-3 Bluetooth Smart Thermostats" } } }, @@ -1748,7 +1769,8 @@ "name": "Fast.com", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "feedreader": { "name": "Feedreader", @@ -1934,7 +1956,7 @@ "folder_watcher": { "name": "Folder Watcher", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "foobot": { @@ -2869,7 +2891,7 @@ "islamic_prayer_times": { "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "calculated" }, "ismartwindow": { "name": "iSmartWindow", @@ -3167,8 +3189,8 @@ "name": "LG", "integrations": { "lg_netcast": { - "integration_type": "hub", - "config_flow": false, + "integration_type": "device", + "config_flow": true, "iot_class": "local_polling", "name": "LG Netcast" }, @@ -3720,7 +3742,8 @@ "moon": { "integration_type": "service", "config_flow": true, - "iot_class": "calculated" + "iot_class": "calculated", + "single_config_entry": true }, "mopeka": { "name": "Mopeka", @@ -3808,7 +3831,8 @@ "name": "Mullvad VPN", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "mutesync": { "name": "mutesync", @@ -3997,7 +4021,8 @@ "name": "NINA", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "nissan_leaf": { "name": "Nissan Leaf", @@ -5161,6 +5186,12 @@ } } }, + "sanix": { + "name": "Sanix", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "satel_integra": { "name": "Satel Integra", "integration_type": "hub", @@ -6457,7 +6488,8 @@ "uptime": { "integration_type": "service", "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "single_config_entry": true }, "uptimerobot": { "name": "UptimeRobot", @@ -7170,6 +7202,7 @@ "demo", "derivative", "emulated_roku", + "energenie_power_sockets", "filesize", "garages_amsterdam", "generic", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index baf922cdc99..7b1bbff9de0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -64,6 +64,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX A21": { + "always_discover": True, + "domain": "lifx", + }, "LIFX BR30": { "always_discover": True, "domain": "lifx", @@ -76,6 +80,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Ceiling": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Clean": { "always_discover": True, "domain": "lifx", @@ -108,6 +116,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Indoor Neon": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Lightstrip": { "always_discover": True, "domain": "lifx", @@ -124,6 +136,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX PAR38": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Pls": { "always_discover": True, "domain": "lifx", @@ -132,6 +148,14 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Round": { + "always_discover": True, + "domain": "lifx", + }, + "LIFX Square": { + "always_discover": True, + "domain": "lifx", + }, "LIFX String": { "always_discover": True, "domain": "lifx", @@ -342,6 +366,12 @@ ZEROCONF = { "macaddress": "b8a44f*", }, }, + { + "domain": "axis", + "properties": { + "macaddress": "e82725*", + }, + }, { "domain": "doorbird", "properties": { @@ -573,6 +603,16 @@ ZEROCONF = { }, }, ], + "_matter._tcp.local.": [ + { + "domain": "matter", + }, + ], + "_matterc._udp.local.": [ + { + "domain": "matter", + }, + ], "_mediaremotetv._tcp.local.": [ { "domain": "apple_tv", diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 15437b00183..2437d42da59 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -22,6 +22,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from homeassistant.util.json import json_loads +from .backports.aiohttp_resolver import AsyncResolver from .frame import warn_use from .json import json_dumps @@ -125,7 +126,7 @@ def async_create_clientsession( if auto_cleanup: auto_cleanup_method = _async_register_clientsession_shutdown - clientsession = _async_create_clientsession( + return _async_create_clientsession( hass, verify_ssl, auto_cleanup_method=auto_cleanup_method, @@ -133,8 +134,6 @@ def async_create_clientsession( **kwargs, ) - return clientsession - @callback def _async_create_clientsession( @@ -310,6 +309,7 @@ def _async_get_connector( ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, + resolver=AsyncResolver(), ) connectors[connector_key] = connector diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index fc535bed610..4dba510396f 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -8,6 +8,7 @@ from typing import Any, Literal, TypedDict, cast from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify +from homeassistant.util.event_type import EventType from . import device_registry as dr, entity_registry as er from .normalized_name_base_registry import ( @@ -20,12 +21,32 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "area_registry" -EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated" +EVENT_AREA_REGISTRY_UPDATED: EventType[EventAreaRegistryUpdatedData] = EventType( + "area_registry_updated" +) STORAGE_KEY = "core.area_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 6 +class _AreaStoreData(TypedDict): + """Data type for individual area. Used in AreasRegistryStoreData.""" + + aliases: list[str] + floor_id: str | None + icon: str | None + id: str + labels: list[str] + name: str + picture: str | None + + +class AreasRegistryStoreData(TypedDict): + """Store data type for AreaRegistry.""" + + areas: list[_AreaStoreData] + + class EventAreaRegistryUpdatedData(TypedDict): """EventAreaRegistryUpdated data.""" @@ -45,7 +66,7 @@ class AreaEntry(NormalizedNameBaseRegistryEntry): picture: str | None -class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): +class AreaRegistryStore(Store[AreasRegistryStoreData]): """Store area registry data.""" async def _async_migrate_func( @@ -53,7 +74,7 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): old_major_version: int, old_minor_version: int, old_data: dict[str, list[dict[str, Any]]], - ) -> dict[str, Any]: + ) -> AreasRegistryStoreData: """Migrate to the new version.""" if old_major_version < 2: if old_minor_version < 2: @@ -84,13 +105,52 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): if old_major_version > 1: raise NotImplementedError - return old_data + return old_data # type: ignore[return-value] -class AreaRegistry(BaseRegistry): +class AreaRegistryItems(NormalizedNameBaseRegistryItems[AreaEntry]): + """Class to hold area registry items.""" + + def __init__(self) -> None: + """Initialize the area registry items.""" + super().__init__() + self._labels_index: dict[str, dict[str, Literal[True]]] = {} + self._floors_index: dict[str, dict[str, Literal[True]]] = {} + + def _index_entry(self, key: str, entry: AreaEntry) -> None: + """Index an entry.""" + if entry.floor_id is not None: + self._floors_index.setdefault(entry.floor_id, {})[key] = True + for label in entry.labels: + self._labels_index.setdefault(label, {})[key] = True + super()._index_entry(key, entry) + + def _unindex_entry( + self, key: str, replacement_entry: AreaEntry | None = None + ) -> None: + entry = self.data[key] + if labels := entry.labels: + for label in labels: + self._unindex_entry_value(key, label, self._labels_index) + if floor_id := entry.floor_id: + self._unindex_entry_value(key, floor_id, self._floors_index) + return super()._unindex_entry(key, replacement_entry) + + def get_areas_for_label(self, label: str) -> list[AreaEntry]: + """Get areas for label.""" + data = self.data + return [data[key] for key in self._labels_index.get(label, ())] + + def get_areas_for_floor(self, floor: str) -> list[AreaEntry]: + """Get areas for floor.""" + data = self.data + return [data[key] for key in self._floors_index.get(floor, ())] + + +class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): """Class to hold a registry of areas.""" - areas: NormalizedNameBaseRegistryItems[AreaEntry] + areas: AreaRegistryItems _area_data: dict[str, AreaEntry] def __init__(self, hass: HomeAssistant) -> None: @@ -142,6 +202,7 @@ class AreaRegistry(BaseRegistry): picture: str | None = None, ) -> AreaEntry: """Create a new area.""" + self.hass.verify_event_loop_thread("async_create") normalized_name = normalize_name(name) if self.async_get_area_by_name(name): @@ -161,14 +222,16 @@ class AreaRegistry(BaseRegistry): assert area.id is not None self.areas[area.id] = area self.async_schedule_save() - self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id} + self.hass.bus.async_fire_internal( + EVENT_AREA_REGISTRY_UPDATED, + EventAreaRegistryUpdatedData(action="create", area_id=area.id), ) return area @callback def async_delete(self, area_id: str) -> None: """Delete area.""" + self.hass.verify_event_loop_thread("async_delete") device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) @@ -176,8 +239,9 @@ class AreaRegistry(BaseRegistry): del self.areas[area_id] - self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "remove", "area_id": area_id} + self.hass.bus.async_fire_internal( + EVENT_AREA_REGISTRY_UPDATED, + EventAreaRegistryUpdatedData(action="remove", area_id=area_id), ) self.async_schedule_save() @@ -204,8 +268,13 @@ class AreaRegistry(BaseRegistry): name=name, picture=picture, ) + # Since updated may be the old or the new and we always fire + # an event even if nothing has changed we cannot use async_fire_internal + # here because we do not know if the thread safety check already + # happened or not in _async_update. self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "update", "area_id": area_id} + EVENT_AREA_REGISTRY_UPDATED, + EventAreaRegistryUpdatedData(action="update", area_id=area_id), ) return updated @@ -243,6 +312,7 @@ class AreaRegistry(BaseRegistry): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update") new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type] self.async_schedule_save() @@ -254,7 +324,7 @@ class AreaRegistry(BaseRegistry): data = await self._store.async_load() - areas = NormalizedNameBaseRegistryItems[AreaEntry]() + areas = AreaRegistryItems() if data is not None: for area in data["areas"]: @@ -275,24 +345,22 @@ class AreaRegistry(BaseRegistry): self._area_data = areas.data @callback - def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: + def _data_to_save(self) -> AreasRegistryStoreData: """Return data of area registry to store in a file.""" - data = {} - - data["areas"] = [ - { - "aliases": list(entry.aliases), - "floor_id": entry.floor_id, - "icon": entry.icon, - "id": entry.id, - "labels": list(entry.labels), - "name": entry.name, - "picture": entry.picture, - } - for entry in self.areas.values() - ] - - return data + return { + "areas": [ + { + "aliases": list(entry.aliases), + "floor_id": entry.floor_id, + "icon": entry.icon, + "id": entry.id, + "labels": list(entry.labels), + "name": entry.name, + "picture": entry.picture, + } + for entry in self.areas.values() + ] + } def _generate_area_id(self, name: str) -> str: """Generate area ID.""" @@ -324,32 +392,26 @@ class AreaRegistry(BaseRegistry): def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) -> None: """Update areas that are associated with a floor that has been removed.""" floor_id = event.data["floor_id"] - for area_id, area in self.areas.items(): - if floor_id == area.floor_id: - self.async_update(area_id, floor_id=None) + for area in self.areas.get_areas_for_floor(floor_id): + self.async_update(area.id, floor_id=None) self.hass.bus.async_listen( event_type=fr.EVENT_FLOOR_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_floor_registry_update, - run_immediately=True, ) @callback def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None: """Update areas that have a label that has been removed.""" label_id = event.data["label_id"] - for area_id, area in self.areas.items(): - if label_id in area.labels: - labels = area.labels.copy() - labels.remove(label_id) - self.async_update(area_id, labels=labels) + for area in self.areas.get_areas_for_label(label_id): + self.async_update(area.id, labels=area.labels - {label_id}) self.hass.bus.async_listen( event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_label_registry_update, - run_immediately=True, ) @@ -369,10 +431,10 @@ async def async_load(hass: HomeAssistant) -> None: @callback def async_entries_for_floor(registry: AreaRegistry, floor_id: str) -> list[AreaEntry]: """Return entries that match a floor.""" - return [area for area in registry.areas.values() if floor_id == area.floor_id] + return registry.areas.get_areas_for_floor(floor_id) @callback 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] + return registry.areas.get_areas_for_label(label_id) diff --git a/homeassistant/helpers/backports/__init__.py b/homeassistant/helpers/backports/__init__.py new file mode 100644 index 00000000000..e672fe1d3d2 --- /dev/null +++ b/homeassistant/helpers/backports/__init__.py @@ -0,0 +1 @@ +"""Backports for helpers.""" diff --git a/homeassistant/helpers/backports/aiohttp_resolver.py b/homeassistant/helpers/backports/aiohttp_resolver.py new file mode 100644 index 00000000000..efa4ba4bb85 --- /dev/null +++ b/homeassistant/helpers/backports/aiohttp_resolver.py @@ -0,0 +1,116 @@ +"""Backport of aiohttp's AsyncResolver for Home Assistant. + +This is a backport of the AsyncResolver class from aiohttp 3.10. + +Before aiohttp 3.10, on system with IPv6 support, AsyncResolver would not fallback +to providing A records when AAAA records were not available. + +Additionally, unlike the ThreadedResolver, AsyncResolver +did not handle link-local addresses correctly. +""" + +from __future__ import annotations + +import asyncio +import socket +import sys +from typing import Any, TypedDict + +import aiodns +from aiohttp.abc import AbstractResolver + +# This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 +# This can be removed once aiohttp 3.10 is the minimum supported version. + +_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV +_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0) + + +class ResolveResult(TypedDict): + """Resolve result. + + This is the result returned from an AbstractResolver's + resolve method. + + :param hostname: The hostname that was provided. + :param host: The IP address that was resolved. + :param port: The port that was resolved. + :param family: The address family that was resolved. + :param proto: The protocol that was resolved. + :param flags: The flags that were resolved. + """ + + hostname: str + host: str + port: int + family: int + proto: int + flags: int + + +class AsyncResolver(AbstractResolver): + """Use the `aiodns` package to make asynchronous DNS lookups.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the resolver.""" + if aiodns is None: + raise RuntimeError("Resolver requires aiodns library") + + self._loop = asyncio.get_running_loop() + self._resolver = aiodns.DNSResolver(*args, loop=self._loop, **kwargs) # type: ignore[misc] + + async def resolve( # type: ignore[override] + self, host: str, port: int = 0, family: int = socket.AF_INET + ) -> list[ResolveResult]: + """Resolve a host name to an IP address.""" + try: + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, # type: ignore[arg-type] + flags=socket.AI_ADDRCONFIG, + ) + except aiodns.error.DNSError as exc: + msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" + raise OSError(msg) from exc + hosts: list[ResolveResult] = [] + for node in resp.nodes: + address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr + family = node.family + if family == socket.AF_INET6: + if len(address) > 3 and address[3] and _SUPPORTS_SCOPE_ID: + # This is essential for link-local IPv6 addresses. + # LL IPv6 is a VERY rare case. Strictly speaking, we should use + # getnameinfo() unconditionally, but performance makes sense. + result = await self._resolver.getnameinfo( + (address[0].decode("ascii"), *address[1:]), + _NUMERIC_SOCKET_FLAGS, + ) + resolved_host = result.node + else: + resolved_host = address[0].decode("ascii") + port = address[1] + else: # IPv4 + assert family == socket.AF_INET + resolved_host = address[0].decode("ascii") + port = address[1] + hosts.append( + ResolveResult( + hostname=host, + host=resolved_host, + port=port, + family=family, + proto=0, + flags=_NUMERIC_SOCKET_FLAGS, + ) + ) + + if not hosts: + raise OSError("DNS lookup failed") + + return hosts + + async def close(self) -> None: + """Close the resolver.""" + self._resolver.cancel() diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index ee0c8c1bb88..4ae920055a2 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -7,18 +7,36 @@ import dataclasses from dataclasses import dataclass, field from typing import Literal, TypedDict, cast -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.util.event_type import EventType from homeassistant.util.ulid import ulid_now from .registry import BaseRegistry -from .typing import UNDEFINED, EventType, UndefinedType +from .storage import Store +from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "category_registry" -EVENT_CATEGORY_REGISTRY_UPDATED = "category_registry_updated" +EVENT_CATEGORY_REGISTRY_UPDATED: EventType[EventCategoryRegistryUpdatedData] = ( + EventType("category_registry_updated") +) STORAGE_KEY = "core.category_registry" STORAGE_VERSION_MAJOR = 1 +class _CategoryStoreData(TypedDict): + """Data type for individual category. Used in CategoryRegistryStoreData.""" + + category_id: str + icon: str | None + name: str + + +class CategoryRegistryStoreData(TypedDict): + """Store data type for CategoryRegistry.""" + + categories: dict[str, list[_CategoryStoreData]] + + class EventCategoryRegistryUpdatedData(TypedDict): """Event data for when the category registry is updated.""" @@ -27,7 +45,7 @@ class EventCategoryRegistryUpdatedData(TypedDict): category_id: str -EventCategoryRegistryUpdated = EventType[EventCategoryRegistryUpdatedData] +EventCategoryRegistryUpdated = Event[EventCategoryRegistryUpdatedData] @dataclass(slots=True, kw_only=True, frozen=True) @@ -39,14 +57,15 @@ class CategoryEntry: name: str -class CategoryRegistry(BaseRegistry): +class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): """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( + self._store = Store( + hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, atomic_writes=True, @@ -165,7 +184,7 @@ class CategoryRegistry(BaseRegistry): self.categories = category_entries @callback - def _data_to_save(self) -> dict[str, dict[str, list[dict[str, str | None]]]]: + def _data_to_save(self) -> CategoryRegistryStoreData: """Return data of category registry to store in a file.""" return { "categories": { diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index e906148efdb..b8c85902f7f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -164,7 +164,7 @@ def trace_condition(variables: TemplateVarsType) -> Generator[TraceElement, None yield trace_element except Exception as ex: trace_element.set_error(ex) - raise ex + raise finally: if should_pop: trace_stack_pop(trace_stack_cv) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 70de144d5c8..bf20a2d7f5f 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -248,13 +248,13 @@ def is_regex(value: Any) -> re.Pattern[Any]: """Validate that a string is a valid regular expression.""" try: r = re.compile(value) - return r except TypeError as err: raise vol.Invalid( f"value {value} is of the wrong type for a regular expression" ) from err except re.error as err: raise vol.Invalid(f"value {value} is not a valid regular expression") from err + return r def isfile(value: Any) -> str: @@ -671,9 +671,9 @@ def template(value: Any | None) -> template_helper.Template: try: template_value.ensure_valid() - return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex + return template_value def dynamic_template(value: Any | None) -> template_helper.Template: @@ -693,9 +693,9 @@ def dynamic_template(value: Any | None) -> template_helper.Template: try: template_value.ensure_valid() - return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex + return template_value def template_complex(value: Any) -> Any: @@ -1106,7 +1106,7 @@ def empty_config_schema(domain: str) -> Callable[[dict], dict]: """Return a config schema which logs if there are configuration parameters.""" def validator(config: dict) -> dict: - if domain in config and config[domain]: + if config_domain := config.get(domain): get_integration_logger(__name__).error( ( "The %s integration does not support any configuration parameters, " @@ -1114,7 +1114,7 @@ def empty_config_schema(domain: str) -> Callable[[dict], dict]: "configuration." ), domain, - config[domain], + config_domain, ) return config diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 1edeb28d88f..2adab32195b 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -18,7 +18,7 @@ from . import config_validation as cv _FlowManagerT = TypeVar( "_FlowManagerT", - bound="data_entry_flow.FlowManager[Any]", + bound=data_entry_flow.FlowManager[Any], default=data_entry_flow.FlowManager, ) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 6e70bbc7635..93520866142 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -243,6 +243,14 @@ class DeprecatedConstantEnum(NamedTuple): breaks_in_ha_version: str | None +class DeprecatedAlias(NamedTuple): + """Deprecated alias.""" + + value: Any + replacement: str + breaks_in_ha_version: str | None + + _PREFIX_DEPRECATED = "_DEPRECATED_" @@ -254,6 +262,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A """ module_name = module_globals.get("__name__") value = replacement = None + description = "constant" if (deprecated_const := module_globals.get(_PREFIX_DEPRECATED + name)) is None: raise AttributeError(f"Module {module_name!r} has no attribute {name!r}") if isinstance(deprecated_const, DeprecatedConstant): @@ -266,6 +275,11 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}" ) breaks_in_ha_version = deprecated_const.breaks_in_ha_version + elif isinstance(deprecated_const, DeprecatedAlias): + description = "alias" + value = deprecated_const.value + replacement = deprecated_const.replacement + breaks_in_ha_version = deprecated_const.breaks_in_ha_version if value is None or replacement is None: msg = ( @@ -284,7 +298,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A name, module_name or __name__, replacement, - "constant", + description, "used", breaks_in_ha_version, log_when_no_integration_is_found=False, diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9666ad302ad..0e64540f11a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,10 +2,9 @@ from __future__ import annotations -from collections import UserDict -from collections.abc import Mapping, ValuesView +from collections.abc import Mapping from enum import StrEnum -from functools import lru_cache, partial +from functools import cached_property, lru_cache, partial import logging import time from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast @@ -13,11 +12,17 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast import attr from yarl import URL -from homeassistant.backports.functools import cached_property from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback, get_release_channel +from homeassistant.core import ( + Event, + HomeAssistant, + ReleaseChannel, + callback, + get_release_channel, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_suggest_report_issue +from homeassistant.util.event_type import EventType from homeassistant.util.json import format_unserializable_data import homeassistant.util.uuid as uuid_util @@ -31,7 +36,7 @@ from .deprecation import ( ) from .frame import report from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment -from .registry import BaseRegistry +from .registry import BaseRegistry, BaseRegistryItems from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -42,7 +47,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = "device_registry" -EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" +EVENT_DEVICE_REGISTRY_UPDATED: EventType[EventDeviceRegistryUpdatedData] = EventType( + "device_registry_updated" +) STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 5 @@ -443,7 +450,7 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): _EntryTypeT = TypeVar("_EntryTypeT", DeviceEntry, DeletedDeviceEntry) -class DeviceRegistryItems(UserDict[str, _EntryTypeT]): +class DeviceRegistryItems(BaseRegistryItems[_EntryTypeT]): """Container for device registry items, maps device id -> entry. Maintains two additional indexes: @@ -457,33 +464,22 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): self._connections: dict[tuple[str, str], _EntryTypeT] = {} self._identifiers: dict[tuple[str, str], _EntryTypeT] = {} - def values(self) -> ValuesView[_EntryTypeT]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: _EntryTypeT) -> None: - """Add an item.""" - data = self.data - if key in data: - old_entry = data[key] - for connection in old_entry.connections: - del self._connections[connection] - for identifier in old_entry.identifiers: - del self._identifiers[identifier] - data[key] = entry + def _index_entry(self, key: str, entry: _EntryTypeT) -> None: + """Index an entry.""" for connection in entry.connections: self._connections[connection] = entry for identifier in entry.identifiers: self._identifiers[identifier] = entry - def __delitem__(self, key: str) -> None: - """Remove an item.""" - entry = self[key] - for connection in entry.connections: + def _unindex_entry( + self, key: str, replacement_entry: _EntryTypeT | None = None + ) -> None: + """Unindex an entry.""" + old_entry = self.data[key] + for connection in old_entry.connections: del self._connections[connection] - for identifier in entry.identifiers: + for identifier in old_entry.identifiers: del self._identifiers[identifier] - super().__delitem__(key) def get_entry( self, @@ -503,10 +499,71 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): return None -class DeviceRegistry(BaseRegistry): +class ActiveDeviceRegistryItems(DeviceRegistryItems[DeviceEntry]): + """Container for active (non-deleted) device registry entries.""" + + def __init__(self) -> None: + """Initialize the container. + + Maintains three additional indexes: + + - area_id -> dict[key, True] + - config_entry_id -> dict[key, True] + - label -> dict[key, True] + """ + super().__init__() + self._area_id_index: dict[str, dict[str, Literal[True]]] = {} + self._config_entry_id_index: dict[str, dict[str, Literal[True]]] = {} + self._labels_index: dict[str, dict[str, Literal[True]]] = {} + + def _index_entry(self, key: str, entry: DeviceEntry) -> None: + """Index an entry.""" + super()._index_entry(key, entry) + if (area_id := entry.area_id) is not None: + self._area_id_index.setdefault(area_id, {})[key] = True + for label in entry.labels: + self._labels_index.setdefault(label, {})[key] = True + for config_entry_id in entry.config_entries: + self._config_entry_id_index.setdefault(config_entry_id, {})[key] = True + + def _unindex_entry( + self, key: str, replacement_entry: DeviceEntry | None = None + ) -> None: + """Unindex an entry.""" + entry = self.data[key] + if area_id := entry.area_id: + self._unindex_entry_value(key, area_id, self._area_id_index) + if labels := entry.labels: + for label in labels: + self._unindex_entry_value(key, label, self._labels_index) + for config_entry_id in entry.config_entries: + self._unindex_entry_value(key, config_entry_id, self._config_entry_id_index) + super()._unindex_entry(key, replacement_entry) + + def get_devices_for_area_id(self, area_id: str) -> list[DeviceEntry]: + """Get devices for area.""" + data = self.data + return [data[key] for key in self._area_id_index.get(area_id, ())] + + def get_devices_for_label(self, label: str) -> list[DeviceEntry]: + """Get devices for label.""" + data = self.data + return [data[key] for key in self._labels_index.get(label, ())] + + def get_devices_for_config_entry_id( + self, config_entry_id: str + ) -> list[DeviceEntry]: + """Get devices for config entry.""" + data = self.data + return [ + data[key] for key in self._config_entry_id_index.get(config_entry_id, ()) + ] + + +class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): """Class to hold a registry of devices.""" - devices: DeviceRegistryItems[DeviceEntry] + devices: ActiveDeviceRegistryItems deleted_devices: DeviceRegistryItems[DeletedDeviceEntry] _device_data: dict[str, DeviceEntry] @@ -557,7 +614,7 @@ class DeviceRegistry(BaseRegistry): try: return name.format(**translation_placeholders) except KeyError as err: - if get_release_channel() != "stable": + if get_release_channel() is not ReleaseChannel.STABLE: raise HomeAssistantError("Missing placeholder %s" % err) from err report_issue = async_suggest_report_issue( self.hass, integration_domain=domain @@ -847,6 +904,7 @@ class DeviceRegistry(BaseRegistry): if not new_values: return old + self.hass.verify_event_loop_thread("async_update_device") new = attr.evolve(old, **new_values) self.devices[device_id] = new @@ -860,20 +918,20 @@ class DeviceRegistry(BaseRegistry): self.async_schedule_save() - data: dict[str, Any] = { - "action": "create" if old.is_new else "update", - "device_id": new.id, - } - if not old.is_new: - data["changes"] = old_values + data: EventDeviceRegistryUpdatedData + if old.is_new: + data = {"action": "create", "device_id": new.id} + else: + data = {"action": "update", "device_id": new.id, "changes": old_values} - self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, data) + self.hass.bus.async_fire_internal(EVENT_DEVICE_REGISTRY_UPDATED, data) return new @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" + self.hass.verify_event_loop_thread("async_remove_device") device = self.devices.pop(device_id) self.deleted_devices[device_id] = DeletedDeviceEntry( config_entries=device.config_entries, @@ -885,8 +943,11 @@ class DeviceRegistry(BaseRegistry): for other_device in list(self.devices.values()): if other_device.via_device_id == device_id: self.async_update_device(other_device.id, via_device_id=None) - self.hass.bus.async_fire( - EVENT_DEVICE_REGISTRY_UPDATED, {"action": "remove", "device_id": device_id} + self.hass.bus.async_fire_internal( + EVENT_DEVICE_REGISTRY_UPDATED, + _EventDeviceRegistryUpdatedData_CreateRemove( + action="remove", device_id=device_id + ), ) self.async_schedule_save() @@ -896,7 +957,7 @@ class DeviceRegistry(BaseRegistry): data = await self._store.async_load() - devices: DeviceRegistryItems[DeviceEntry] = DeviceRegistryItems() + devices = ActiveDeviceRegistryItems() deleted_devices: DeviceRegistryItems[DeletedDeviceEntry] = DeviceRegistryItems() if data is not None: @@ -910,12 +971,16 @@ class DeviceRegistry(BaseRegistry): tuple(conn) # type: ignore[misc] for conn in device["connections"] }, - disabled_by=DeviceEntryDisabler(device["disabled_by"]) - if device["disabled_by"] - else None, - entry_type=DeviceEntryType(device["entry_type"]) - if device["entry_type"] - else None, + disabled_by=( + DeviceEntryDisabler(device["disabled_by"]) + if device["disabled_by"] + else None + ), + entry_type=( + DeviceEntryType(device["entry_type"]) + if device["entry_type"] + else None + ), hw_version=device["hw_version"], id=device["id"], identifiers={ @@ -959,7 +1024,7 @@ class DeviceRegistry(BaseRegistry): def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" now_time = time.time() - for device in list(self.devices.values()): + for device in self.devices.get_devices_for_config_entry_id(config_entry_id): self.async_update_device(device.id, remove_config_entry_id=config_entry_id) for deleted_device in list(self.deleted_devices.values()): config_entries = deleted_device.config_entries @@ -1000,18 +1065,14 @@ class DeviceRegistry(BaseRegistry): @callback def async_clear_area_id(self, area_id: str) -> None: """Clear area id from registry entries.""" - for dev_id, device in self.devices.items(): - if area_id == device.area_id: - self.async_update_device(dev_id, area_id=None) + for device in self.devices.get_devices_for_area_id(area_id): + self.async_update_device(device.id, area_id=None) @callback def async_clear_label_id(self, label_id: str) -> None: """Clear label from registry entries.""" - for device_id, entry in self.devices.items(): - if label_id in entry.labels: - labels = entry.labels.copy() - labels.remove(label_id) - self.async_update_device(device_id, labels=labels) + for device in self.devices.get_devices_for_label(label_id): + self.async_update_device(device.id, labels=device.labels - {label_id}) @callback @@ -1030,7 +1091,7 @@ async def async_load(hass: HomeAssistant) -> None: @callback def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> list[DeviceEntry]: """Return entries that match an area.""" - return [device for device in registry.devices.values() if device.area_id == area_id] + return registry.devices.get_devices_for_area_id(area_id) @callback @@ -1038,7 +1099,7 @@ def async_entries_for_label( registry: DeviceRegistry, label_id: str ) -> list[DeviceEntry]: """Return entries that match a label.""" - return [device for device in registry.devices.values() if label_id in device.labels] + return registry.devices.get_devices_for_label(label_id) @callback @@ -1046,11 +1107,7 @@ def async_entries_for_config_entry( registry: DeviceRegistry, config_entry_id: str ) -> list[DeviceEntry]: """Return entries that match a config entry.""" - return [ - device - for device in registry.devices.values() - if config_entry_id in device.config_entries - ] + return registry.devices.get_devices_for_config_entry_id(config_entry_id) @callback @@ -1158,7 +1215,6 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, event_filter=_label_removed_from_registry_filter, listener=_handle_label_registry_update, - run_immediately=True, ) @callback @@ -1172,12 +1228,16 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: ) @callback - def _async_entity_registry_changed(event: Event) -> None: + def _async_entity_registry_changed( + event: Event[entity_registry.EventEntityRegistryUpdatedData], + ) -> None: """Handle entity updated or removed dispatch.""" debounced_cleanup.async_schedule_call() @callback - def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool: + def entity_registry_changed_filter( + event_data: entity_registry.EventEntityRegistryUpdatedData, + ) -> bool: """Handle entity updated or removed filter.""" if ( event_data["action"] == "update" @@ -1187,28 +1247,24 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: return True - if hass.is_running: + def _async_listen_for_cleanup() -> None: + """Listen for entity registry changes.""" hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_changed, event_filter=entity_registry_changed_filter, - run_immediately=True, ) + + if hass.is_running: + _async_listen_for_cleanup() return async def startup_clean(event: Event) -> None: """Clean up on startup.""" - hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - _async_entity_registry_changed, - event_filter=entity_registry_changed_filter, - run_immediately=True, - ) + _async_listen_for_cleanup() await debounced_cleanup.async_call() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, startup_clean, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) @callback def _on_homeassistant_stop(event: Event) -> None: diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 4b5a0117be7..2e14759b814 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -152,8 +152,11 @@ async def async_load_platform( Use `async_listen_platform` to register a callback for these events. - Warning: Do not await this inside a setup method to avoid a dead lock. - Use `hass.async_create_task(async_load_platform(..))` instead. + Warning: This method can load a base component if its not loaded which + can take a long time since base components currently have to import + every platform integration listed under it to do config validation. + To avoid waiting for this, use + `hass.async_create_task(async_load_platform(..))` instead. """ assert hass_config is not None, "You need to pass in the real hass config" diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index e24b405c685..e479a47ecfd 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -11,7 +11,7 @@ from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util.async_ import gather_with_limited_concurrency -FLOW_INIT_LIMIT = 2 +FLOW_INIT_LIMIT = 20 DISCOVERY_FLOW_DISPATCHER = "discovery_flow_dispatcher" @@ -30,7 +30,7 @@ def async_create_flow( if not dispatcher or dispatcher.started: if init_coro := _async_init_flow(hass, domain, context, data): - hass.async_create_task( + hass.async_create_background_task( init_coro, f"discovery flow {domain} {context}", eager_start=True ) return @@ -82,9 +82,7 @@ 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, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_start) 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 c1194c7da01..aa8176a1b83 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -7,7 +7,12 @@ from functools import partial import logging from typing import Any, TypeVarTuple, overload -from homeassistant.core import HassJob, HomeAssistant, callback +from homeassistant.core import ( + HassJob, + HomeAssistant, + callback, + get_hassjob_callable_job_type, +) from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception @@ -161,9 +166,13 @@ def _generate_job( signal: SignalType[*_Ts] | str, target: Callable[[*_Ts], Any] | Callable[..., Any] ) -> HassJob[..., None | Coroutine[Any, Any, None]]: """Generate a HassJob for a signal and target.""" + job_type = get_hassjob_callable_job_type(target) return HassJob( - catch_log_exception(target, partial(_format_err, signal, target)), + catch_log_exception( + target, partial(_format_err, signal, target), job_type=job_type + ), f"dispatcher {signal}", + job_type=job_type, ) @@ -190,6 +199,9 @@ def async_dispatcher_send( This method must be run in the event loop. """ + if hass.config.debug: + hass.verify_event_loop_thread("async_dispatcher_send") + if (maybe_dispatchers := hass.data.get(DATA_DISPATCHER)) is None: return dispatchers: _DispatcherDataType[*_Ts] = maybe_dispatchers diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 988ce29ade2..6352a56dc90 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -5,10 +5,11 @@ from __future__ import annotations from abc import ABCMeta import asyncio from collections import deque -from collections.abc import Callable, Coroutine, Iterable, Mapping, MutableMapping +from collections.abc import Callable, Coroutine, Iterable, Mapping import dataclasses from enum import Enum, IntFlag, auto import functools as ft +from functools import cached_property import logging import math from operator import attrgetter @@ -51,6 +52,7 @@ from homeassistant.core import ( Event, HassJobType, HomeAssistant, + ReleaseChannel, callback, get_hassjob_callable_job_type, get_release_channel, @@ -73,11 +75,7 @@ from .event import ( from .typing import UNDEFINED, StateType, UndefinedType if TYPE_CHECKING: - from functools import cached_property - from .entity_platform import EntityPlatform -else: - from homeassistant.backports.functools import cached_property _T = TypeVar("_T") @@ -523,6 +521,7 @@ class Entity( # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. _state_info: StateInfo = None # type: ignore[assignment] + _is_custom_component: bool = False __capabilities_updated_at: deque[float] __capabilities_updated_at_reported: bool = False @@ -540,7 +539,7 @@ class Entity( _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_entity_registry_visible_default: bool - _attr_extra_state_attributes: MutableMapping[str, Any] + _attr_extra_state_attributes: dict[str, Any] _attr_force_update: bool _attr_icon: str | None _attr_name: str | None @@ -660,7 +659,7 @@ class Entity( return name.format(**self.translation_placeholders) except KeyError as err: if not self._name_translation_placeholders_reported: - if get_release_channel() != "stable": + if get_release_channel() is not ReleaseChannel.STABLE: raise HomeAssistantError("Missing placeholder %s" % err) from err report_issue = self._suggest_report_issue() _LOGGER.warning( @@ -969,8 +968,8 @@ class Entity( self._async_write_ha_state() @callback - def async_write_ha_state(self) -> None: - """Write the state to the state machine.""" + def _async_verify_state_writable(self) -> None: + """Verify the entity is in a writable state.""" if self.hass is None: raise RuntimeError(f"Attribute hass is None for {self}") @@ -995,6 +994,18 @@ class Entity( f"No entity id specified for entity {self.name}" ) + @callback + def _async_write_ha_state_from_call_soon_threadsafe(self) -> None: + """Write the state to the state machine from the event loop thread.""" + self._async_verify_state_writable() + self._async_write_ha_state() + + @callback + def async_write_ha_state(self) -> None: + """Write the state to the state machine.""" + self._async_verify_state_writable() + if self._is_custom_component or self.hass.config.debug: + self.hass.verify_event_loop_thread("async_write_ha_state") self._async_write_ha_state() def _stringify_state(self, available: bool) -> str: @@ -1055,8 +1066,10 @@ class Entity( available = self.available # only call self.available once per update cycle state = self._stringify_state(available) if available: - attr.update(self.state_attributes or {}) - attr.update(self.extra_state_attributes or {}) + if state_attributes := self.state_attributes: + attr.update(state_attributes) + if extra_state_attributes := self.extra_state_attributes: + attr.update(extra_state_attributes) if (unit_of_measurement := self.unit_of_measurement) is not None: attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement @@ -1095,7 +1108,7 @@ class Entity( @callback def _async_write_ha_state(self) -> None: """Write the state to the state machine.""" - if self._platform_state == EntityPlatformState.REMOVED: + if self._platform_state is EntityPlatformState.REMOVED: # Polling returned after the entity has already been removed return @@ -1131,7 +1144,16 @@ class Entity( ): if not self.__capabilities_updated_at_reported: time_now = hass.loop.time() - capabilities_updated_at = self.__capabilities_updated_at + # _Entity__capabilities_updated_at is because of name mangling + if not ( + capabilities_updated_at := getattr( + self, "_Entity__capabilities_updated_at", None + ) + ): + self.__capabilities_updated_at = deque( + maxlen=CAPABILITIES_UPDATE_LIMIT + 1 + ) + capabilities_updated_at = self.__capabilities_updated_at capabilities_updated_at.append(time_now) while time_now - capabilities_updated_at[0] > 3600: capabilities_updated_at.popleft() @@ -1210,7 +1232,9 @@ class Entity( f"Entity {self.entity_id} schedule update ha state", ) else: - self.hass.loop.call_soon_threadsafe(self.async_write_ha_state) + self.hass.loop.call_soon_threadsafe( + self._async_write_ha_state_from_call_soon_threadsafe + ) @callback def async_schedule_update_ha_state(self, force_refresh: bool = False) -> None: @@ -1228,6 +1252,7 @@ class Entity( self.hass.async_create_task( self.async_update_ha_state(force_refresh), f"Entity schedule update ha state {self.entity_id}", + eager_start=True, ) else: self.async_write_ha_state() @@ -1298,7 +1323,7 @@ class Entity( parallel_updates: asyncio.Semaphore | None, ) -> None: """Start adding an entity to a platform.""" - if self._platform_state != EntityPlatformState.NOT_ADDED: + if self._platform_state is not EntityPlatformState.NOT_ADDED: raise HomeAssistantError( f"Entity '{self.entity_id}' cannot be added a second time to an entity" " platform" @@ -1414,10 +1439,12 @@ class Entity( Not to be extended by integrations. """ + is_custom_component = "custom_components" in type(self).__module__ entity_info: EntityInfo = { "domain": self.platform.platform_name, - "custom_component": "custom_components" in type(self).__module__, + "custom_component": is_custom_component, } + self._is_custom_component = is_custom_component if self.platform.config_entry: entity_info["config_entry"] = self.platform.config_entry.entry_id @@ -1444,8 +1471,6 @@ class Entity( ) self._async_subscribe_device_updates() - self.__capabilities_updated_at = deque(maxlen=CAPABILITIES_UPDATE_LIMIT + 1) - async def async_internal_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass. @@ -1465,7 +1490,7 @@ class Entity( is_remove = action == "remove" self._removed_from_registry = is_remove if action == "update" or is_remove: - self.hass.async_create_task( + self.hass.async_create_task_internal( self._async_process_registry_update_or_remove(event), eager_start=True ) @@ -1567,7 +1592,7 @@ class Entity( If the entity is not added to a platform it's not safe to call _stringify_state. """ - if self._platform_state != EntityPlatformState.ADDED: + if self._platform_state is not EntityPlatformState.ADDED: return f"" return f"" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index b764a29a686..eb54d83e1dd 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -120,9 +120,7 @@ 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, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) def setup(self, config: ConfigType) -> None: """Set up a full entity component. @@ -148,7 +146,7 @@ class EntityComponent(Generic[_EntityT]): # Look in config for Domain, Domain 2, Domain 3 etc and load them for p_type, p_config in conf_util.config_per_platform(config, self.domain): if p_type is not None: - self.hass.async_create_task( + self.hass.async_create_task_internal( self.async_setup_platform(p_type, p_config), f"EntityComponent setup platform {p_type} {self.domain}", eager_start=True, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6d7ed7ed1b8..f95c0a0b66a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -362,10 +362,6 @@ class EntityPlatform: 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 @@ -417,6 +413,10 @@ class EntityPlatform: self.domain, ) return False + else: + hass.config.components.add(full_name) + self._setup_complete = True + return True finally: warn_task.cancel() @@ -477,7 +477,7 @@ class EntityPlatform: self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Schedule adding entities for a single platform async.""" - task = self.hass.async_create_task( + task = self.hass.async_create_task_internal( self.async_add_entities(new_entities, update_before_add=update_before_add), f"EntityPlatform async_add_entities {self.domain}.{self.platform_name}", eager_start=True, @@ -801,7 +801,7 @@ class EntityPlatform: get_initial_options=entity.get_initial_entity_options, has_entity_name=entity.has_entity_name, hidden_by=hidden_by, - known_object_ids=self.entities.keys(), + known_object_ids=self.entities, original_device_class=entity.device_class, original_icon=entity.icon, original_name=entity_name, @@ -839,11 +839,13 @@ class EntityPlatform: if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" entity.entity_id = entity_registry.async_generate_entity_id( - self.domain, suggested_object_id, self.entities.keys() + self.domain, suggested_object_id, self.entities ) # Make sure it is valid in case an entity set the value themselves - if not valid_entity_id(entity.entity_id): + # Avoid calling valid_entity_id if we already know it is valid + # since it already made it in the registry + if not entity.registry_entry and not valid_entity_id(entity.entity_id): entity.add_to_platform_abort() raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}") diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index ef9274c6ceb..589b379cf08 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,10 +10,10 @@ timer. from __future__ import annotations -from collections import UserDict -from collections.abc import Callable, Iterable, KeysView, Mapping, ValuesView +from collections.abc import Callable, Container, Hashable, KeysView, Mapping from datetime import datetime, timedelta from enum import StrEnum +from functools import cached_property import logging import time from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, TypeVar, cast @@ -21,7 +21,6 @@ from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, TypeVar, import attr import voluptuous as vol -from homeassistant.backports.functools import cached_property from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -46,14 +45,19 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.exceptions import MaxLengthExceeded +from homeassistant.loader import async_suggest_report_issue from homeassistant.util import slugify, uuid as uuid_util +from homeassistant.util.event_type import EventType from homeassistant.util.json import format_unserializable_data from homeassistant.util.read_only_dict import ReadOnlyDict from . import device_registry as dr, storage -from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from .device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + EventDeviceRegistryUpdatedData, +) from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment -from .registry import BaseRegistry +from .registry import BaseRegistry, BaseRegistryItems from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -62,7 +66,9 @@ if TYPE_CHECKING: T = TypeVar("T") DATA_REGISTRY = "entity_registry" -EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" +EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = EventType( + "entity_registry_updated" +) _LOGGER = logging.getLogger(__name__) @@ -243,7 +249,6 @@ class RegistryEntry: try: dict_repr = self._as_display_dict json_repr: bytes | None = json_bytes(dict_repr) if dict_repr else None - return json_repr except (ValueError, TypeError): _LOGGER.error( "Unable to serialize entry %s to JSON. Bad data found at %s", @@ -252,8 +257,8 @@ class RegistryEntry: find_paths_unserializable_data(dict_repr, dump=JSON_DUMP) ), ) - - return None + return None + return json_repr @cached_property def as_partial_dict(self) -> dict[str, Any]: @@ -511,14 +516,16 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): return data -class EntityRegistryItems(UserDict[str, RegistryEntry]): +class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): """Container for entity registry items, maps entity_id -> entry. - Maintains four additional indexes: + Maintains six additional indexes: - id -> entry - (domain, platform, unique_id) -> entity_id - - config_entry_id -> list[key] - - device_id -> list[key] + - config_entry_id -> dict[key, True] + - device_id -> dict[key, True] + - area_id -> dict[key, True] + - label -> dict[key, True] """ def __init__(self) -> None: @@ -529,17 +536,10 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._config_entry_id_index: dict[str, dict[str, Literal[True]]] = {} self._device_id_index: dict[str, dict[str, Literal[True]]] = {} self._area_id_index: dict[str, dict[str, Literal[True]]] = {} + self._labels_index: dict[str, dict[str, Literal[True]]] = {} - def values(self) -> ValuesView[RegistryEntry]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: RegistryEntry) -> None: - """Add an item.""" - data = self.data - if key in data: - self._unindex_entry(key) - data[key] = entry + def _index_entry(self, key: str, entry: RegistryEntry) -> None: + """Index an entry.""" self._entry_ids[entry.id] = entry self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id # python has no ordered set, so we use a dict with True values @@ -550,22 +550,12 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._device_id_index.setdefault(device_id, {})[key] = True if (area_id := entry.area_id) is not None: self._area_id_index.setdefault(area_id, {})[key] = True + for label in entry.labels: + self._labels_index.setdefault(label, {})[key] = True - def _unindex_entry_value( - self, key: str, value: str, index: dict[str, dict[str, Literal[True]]] + def _unindex_entry( + self, key: str, replacement_entry: RegistryEntry | None = None ) -> None: - """Unindex an entry value. - - key is the entry key - value is the value to unindex such as config_entry_id or device_id. - index is the index to unindex from. - """ - entries = index[value] - del entries[key] - if not entries: - del index[value] - - def _unindex_entry(self, key: str) -> None: """Unindex an entry.""" entry = self.data[key] del self._entry_ids[entry.id] @@ -576,11 +566,9 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._unindex_entry_value(key, device_id, self._device_id_index) if area_id := entry.area_id: self._unindex_entry_value(key, area_id, self._area_id_index) - - def __delitem__(self, key: str) -> None: - """Remove an item.""" - self._unindex_entry(key) - super().__delitem__(key) + if labels := entry.labels: + for label in labels: + self._unindex_entry_value(key, label, self._labels_index) def get_device_ids(self) -> KeysView[str]: """Return device ids.""" @@ -619,6 +607,60 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): data = self.data return [data[key] for key in self._area_id_index.get(area_id, ())] + def get_entries_for_label(self, label: str) -> list[RegistryEntry]: + """Get entries for label.""" + data = self.data + return [data[key] for key in self._labels_index.get(label, ())] + + +def _validate_item( + hass: HomeAssistant, + domain: str, + platform: str, + unique_id: str | Hashable | UndefinedType | Any, + *, + disabled_by: RegistryEntryDisabler | None | UndefinedType = None, + entity_category: EntityCategory | None | UndefinedType = None, + hidden_by: RegistryEntryHider | None | UndefinedType = None, +) -> None: + """Validate entity registry item.""" + if unique_id is not UNDEFINED and not isinstance(unique_id, Hashable): + raise TypeError(f"unique_id must be a string, got {unique_id}") + if unique_id is not UNDEFINED and not isinstance(unique_id, str): + # In HA Core 2025.4, we should fail if unique_id is not a string + report_issue = async_suggest_report_issue(hass, integration_domain=platform) + _LOGGER.error( + ("'%s' from integration %s has a non string unique_id" " '%s', please %s"), + domain, + platform, + unique_id, + report_issue, + ) + if ( + disabled_by + and disabled_by is not UNDEFINED + and not isinstance(disabled_by, RegistryEntryDisabler) + ): + raise ValueError( + f"disabled_by must be a RegistryEntryDisabler value, got {disabled_by}" + ) + if ( + entity_category + and entity_category is not UNDEFINED + and not isinstance(entity_category, EntityCategory) + ): + raise ValueError( + f"entity_category must be a valid EntityCategory instance, got {entity_category}" + ) + if ( + hidden_by + and hidden_by is not UNDEFINED + and not isinstance(hidden_by, RegistryEntryHider) + ): + raise ValueError( + f"hidden_by must be a RegistryEntryHider value, got {hidden_by}" + ) + class EntityRegistry(BaseRegistry): """Class to hold a registry of entities.""" @@ -640,7 +682,6 @@ class EntityRegistry(BaseRegistry): self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified, - run_immediately=True, ) @callback @@ -672,7 +713,7 @@ class EntityRegistry(BaseRegistry): return list(self.entities.get_device_ids()) def _entity_id_available( - self, entity_id: str, known_object_ids: Iterable[str] | None + self, entity_id: str, known_object_ids: Container[str] | None ) -> bool: """Return True if the entity_id is available. @@ -698,7 +739,7 @@ class EntityRegistry(BaseRegistry): self, domain: str, suggested_object_id: str, - known_object_ids: Iterable[str] | None = None, + known_object_ids: Container[str] | None = None, ) -> str: """Generate an entity ID that does not conflict. @@ -711,7 +752,7 @@ class EntityRegistry(BaseRegistry): test_string = preferred_string[:MAX_LENGTH_STATE_ENTITY_ID] if known_object_ids is None: - known_object_ids = {} + known_object_ids = set() tries = 1 while not self._entity_id_available(test_string, known_object_ids): @@ -731,7 +772,7 @@ class EntityRegistry(BaseRegistry): unique_id: str, *, # To influence entity ID generation - known_object_ids: Iterable[str] | None = None, + known_object_ids: Container[str] | None = None, suggested_object_id: str | None = None, # To disable or hide an entity if it gets created disabled_by: RegistryEntryDisabler | None = None, @@ -778,6 +819,17 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=unit_of_measurement, ) + self.hass.verify_event_loop_thread("async_get_or_create") + _validate_item( + self.hass, + domain, + platform, + disabled_by=disabled_by, + entity_category=entity_category, + hidden_by=hidden_by, + unique_id=unique_id, + ) + entity_registry_id: str | None = None deleted_entity = self.deleted_entities.pop((domain, platform, unique_id), None) if deleted_entity is not None: @@ -790,11 +842,6 @@ class EntityRegistry(BaseRegistry): known_object_ids, ) - if disabled_by and not isinstance(disabled_by, RegistryEntryDisabler): - raise ValueError("disabled_by must be a RegistryEntryDisabler value") - if hidden_by and not isinstance(hidden_by, RegistryEntryHider): - raise ValueError("hidden_by must be a RegistryEntryHider value") - if ( disabled_by is None and config_entry @@ -803,13 +850,6 @@ class EntityRegistry(BaseRegistry): ): disabled_by = RegistryEntryDisabler.INTEGRATION - if ( - entity_category - and entity_category is not UNDEFINED - and not isinstance(entity_category, EntityCategory) - ): - raise ValueError("entity_category must be a valid EntityCategory instance") - def none_if_undefined(value: T | UndefinedType) -> T | None: """Return None if value is UNDEFINED, otherwise return value.""" return None if value is UNDEFINED else value @@ -840,8 +880,11 @@ class EntityRegistry(BaseRegistry): _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) self.async_schedule_save() - self.hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": entity_id} + self.hass.bus.async_fire_internal( + EVENT_ENTITY_REGISTRY_UPDATED, + _EventEntityRegistryUpdatedData_CreateRemove( + action="create", entity_id=entity_id + ), ) return entry @@ -849,6 +892,7 @@ class EntityRegistry(BaseRegistry): @callback def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" + self.hass.verify_event_loop_thread("async_remove") entity = self.entities.pop(entity_id) config_entry_id = entity.config_entry_id key = (entity.domain, entity.platform, entity.unique_id) @@ -862,13 +906,18 @@ class EntityRegistry(BaseRegistry): platform=entity.platform, unique_id=entity.unique_id, ) - self.hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": entity_id} + self.hass.bus.async_fire_internal( + EVENT_ENTITY_REGISTRY_UPDATED, + _EventEntityRegistryUpdatedData_CreateRemove( + action="remove", entity_id=entity_id + ), ) self.async_schedule_save() @callback - def async_device_modified(self, event: Event) -> None: + def async_device_modified( + self, event: Event[EventDeviceRegistryUpdatedData] + ) -> None: """Handle the removal or update of a device. Remove entities from the registry that are associated to a device when @@ -968,26 +1017,6 @@ class EntityRegistry(BaseRegistry): new_values: dict[str, Any] = {} # Dict with new key/value pairs old_values: dict[str, Any] = {} # Dict with old key/value pairs - if ( - disabled_by - and disabled_by is not UNDEFINED - and not isinstance(disabled_by, RegistryEntryDisabler) - ): - raise ValueError("disabled_by must be a RegistryEntryDisabler value") - if ( - hidden_by - and hidden_by is not UNDEFINED - and not isinstance(hidden_by, RegistryEntryHider) - ): - raise ValueError("hidden_by must be a RegistryEntryHider value") - - if ( - entity_category - and entity_category is not UNDEFINED - and not isinstance(entity_category, EntityCategory) - ): - raise ValueError("entity_category must be a valid EntityCategory instance") - for attr_name, value in ( ("aliases", aliases), ("area_id", area_id), @@ -1016,6 +1045,18 @@ class EntityRegistry(BaseRegistry): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) + # Only validate if data has changed + if new_values or new_unique_id is not UNDEFINED: + _validate_item( + self.hass, + old.domain, + old.platform, + disabled_by=disabled_by, + entity_category=entity_category, + hidden_by=hidden_by, + unique_id=new_unique_id, + ) + if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if not self._entity_id_available(new_entity_id, None): raise ValueError("Entity with this ID is already registered") @@ -1046,11 +1087,13 @@ class EntityRegistry(BaseRegistry): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update_entity") + new = self.entities[entity_id] = attr.evolve(old, **new_values) self.async_schedule_save() - data: dict[str, str | dict[str, Any]] = { + data: _EventEntityRegistryUpdatedData_Update = { "action": "update", "entity_id": entity_id, "changes": old_values, @@ -1059,7 +1102,7 @@ class EntityRegistry(BaseRegistry): if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id - self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) + self.hass.bus.async_fire_internal(EVENT_ENTITY_REGISTRY_UPDATED, data) return new @@ -1184,6 +1227,27 @@ class EntityRegistry(BaseRegistry): if entity["entity_category"] == "system": entity["entity_category"] = None + try: + domain = split_entity_id(entity["entity_id"])[0] + _validate_item( + self.hass, domain, entity["platform"], entity["unique_id"] + ) + except (TypeError, ValueError) as err: + report_issue = async_suggest_report_issue( + self.hass, integration_domain=entity["platform"] + ) + _LOGGER.error( + ( + "Entity registry entry '%s' from integration %s could not " + "be loaded: '%s', please %s" + ), + entity["entity_id"], + entity["platform"], + str(err), + report_issue, + ) + continue + entities[entity["entity_id"]] = RegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], @@ -1219,6 +1283,13 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=entity["unit_of_measurement"], ) for entity in data["deleted_entities"]: + try: + domain = split_entity_id(entity["entity_id"])[0] + _validate_item( + self.hass, domain, entity["platform"], entity["unique_id"] + ) + except (TypeError, ValueError): + continue key = ( split_entity_id(entity["entity_id"])[0], entity["platform"], @@ -1261,11 +1332,8 @@ class EntityRegistry(BaseRegistry): @callback def async_clear_label_id(self, label_id: str) -> None: """Clear label from registry entries.""" - for entity_id, entry in self.entities.items(): - if label_id in entry.labels: - labels = entry.labels.copy() - labels.remove(label_id) - self.async_update_entity(entity_id, labels=labels) + for entry in self.entities.get_entries_for_label(label_id): + self.async_update_entity(entry.entity_id, labels=entry.labels - {label_id}) @callback def async_clear_config_entry(self, config_entry_id: str) -> None: @@ -1344,7 +1412,7 @@ def async_entries_for_label( registry: EntityRegistry, label_id: str ) -> list[RegistryEntry]: """Return entries that match a label.""" - return [entry for entry in registry.entities.values() if label_id in entry.labels] + return registry.entities.get_entries_for_label(label_id) @callback @@ -1423,7 +1491,6 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_label_registry_update, - run_immediately=True, ) @callback @@ -1437,7 +1504,6 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: event_type=cr.EVENT_CATEGORY_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_category_registry_update, - run_immediately=True, ) @callback @@ -1456,9 +1522,7 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: """Cancel cleanup.""" cancel() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop) @callback @@ -1471,7 +1535,7 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - return bool(event_data["action"] == "remove") @callback - def cleanup_restored_states(event: Event) -> None: + def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None: """Clean up restored states.""" state = hass.states.get(event.data["entity_id"]) @@ -1484,7 +1548,6 @@ 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: @@ -1501,9 +1564,7 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - entry.write_unavailable_state(hass) - hass.bus.async_listen( - EVENT_HOMEASSISTANT_START, _write_unavailable_states, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) async def async_migrate_entries( diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 749c6d3e6e4..5cffe992c0d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -3,25 +3,16 @@ from __future__ import annotations import asyncio +from collections import defaultdict from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence import copy from dataclasses import dataclass from datetime import datetime, timedelta -import functools as ft +from functools import partial, wraps import logging from random import randint import time -from typing import ( - TYPE_CHECKING, - Any, - Concatenate, - Generic, - ParamSpec, - TypedDict, - TypeVar, -) - -import attr +from typing import TYPE_CHECKING, Any, Concatenate, Generic, ParamSpec, TypeVar from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, @@ -33,6 +24,8 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + # Explicit reexport of 'EventStateChangedData' for backwards compatibility + EventStateChangedData as EventStateChangedData, # noqa: PLC0414 HassJob, HassJobType, HomeAssistant, @@ -44,7 +37,9 @@ from homeassistant.exceptions import TemplateError from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.event_type import EventType +from . import frame from .device_registry import ( EVENT_DEVICE_REGISTRY_UPDATED, EventDeviceRegistryUpdatedData, @@ -96,7 +91,7 @@ class _KeyedEventTracker(Generic[_TypedDictT]): listeners_key: str callbacks_key: str - event_type: str + event_type: EventType[_TypedDictT] | str dispatcher_callable: Callable[ [ HomeAssistant, @@ -113,7 +108,6 @@ class _KeyedEventTracker(Generic[_TypedDictT]): ], bool, ] - run_immediately: bool @dataclass(slots=True) @@ -163,20 +157,12 @@ class TrackTemplateResult: result: Any -class EventStateChangedData(TypedDict): - """EventStateChanged data.""" - - entity_id: str - old_state: State | None - new_state: State | None - - def threaded_listener_factory( async_factory: Callable[Concatenate[HomeAssistant, _P], Any], ) -> Callable[Concatenate[HomeAssistant, _P], CALLBACK_TYPE]: """Convert an async event helper to a threaded one.""" - @ft.wraps(async_factory) + @wraps(async_factory) def factory( hass: HomeAssistant, *args: _P.args, **kwargs: _P.kwargs ) -> CALLBACK_TYPE: @@ -185,7 +171,7 @@ def threaded_listener_factory( raise TypeError("First parameter needs to be a hass instance") async_remove = run_callback_threadsafe( - hass.loop, ft.partial(async_factory, hass, *args, **kwargs) + hass.loop, partial(async_factory, hass, *args, **kwargs) ).result() def remove() -> None: @@ -219,8 +205,16 @@ def async_track_state_change( being None, async_track_state_change_event should be used instead as it is slightly faster. + This function is deprecated and will be removed in Home Assistant 2025.5. + Must be run within the event loop. """ + frame.report( + "calls `async_track_state_change` instead of `async_track_state_change_event`" + " which is deprecated and will be removed in Home Assistant 2025.5", + error_if_core=False, + ) + if from_state is not None: match_from_state = process_state_match(from_state) if to_state is not None: @@ -287,7 +281,9 @@ 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, event_filter=state_change_filter + EVENT_STATE_CHANGED, + state_change_dispatcher, + event_filter=state_change_filter, ) @@ -353,7 +349,6 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_entity_id_event, filter_callable=_async_state_change_filter, - run_immediately=False, ) @@ -414,35 +409,34 @@ def _async_track_event( if not keys: return _remove_empty_listener - if isinstance(keys, str): - keys = [keys] - hass_data = hass.data - callbacks_key = tracker.callbacks_key - - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]] | None - if not (callbacks := hass_data.get(callbacks_key)): - callbacks = hass_data[callbacks_key] = {} + callbacks: defaultdict[str, list[HassJob[[Event[_TypedDictT]], Any]]] | None + if not (callbacks := hass_data.get(tracker.callbacks_key)): + callbacks = hass_data[tracker.callbacks_key] = defaultdict(list) listeners_key = tracker.listeners_key - - if listeners_key not in hass_data: - hass_data[listeners_key] = hass.bus.async_listen( + if tracker.listeners_key not in hass_data: + hass_data[tracker.listeners_key] = hass.bus.async_listen( tracker.event_type, - ft.partial(tracker.dispatcher_callable, hass, callbacks), - event_filter=ft.partial(tracker.filter_callable, hass, callbacks), - run_immediately=tracker.run_immediately, + partial(tracker.dispatcher_callable, hass, callbacks), + event_filter=partial(tracker.filter_callable, hass, callbacks), ) job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type) - for key in keys: - if callback_list := callbacks.get(key): - callback_list.append(job) - else: - callbacks[key] = [job] + if isinstance(keys, str): + # Almost all calls to this function use a single key + # so we optimize for that case. We don't use setdefault + # here because this function gets called ~20000 times + # during startup, and we want to avoid the overhead of + # creating empty lists and throwing them away. + callbacks[keys].append(job) + keys = [keys] + else: + for key in keys: + callbacks[key].append(job) - return ft.partial(_remove_listener, hass, listeners_key, keys, job, callbacks) + return partial(_remove_listener, hass, listeners_key, keys, job, callbacks) @callback @@ -485,7 +479,6 @@ _KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker( event_type=EVENT_ENTITY_REGISTRY_UPDATED, dispatcher_callable=_async_dispatch_old_entity_id_or_entity_id_event, filter_callable=_async_entity_registry_updated_filter, - run_immediately=True, ) @@ -544,7 +537,6 @@ _KEYED_TRACK_DEVICE_REGISTRY_UPDATED = _KeyedEventTracker( event_type=EVENT_DEVICE_REGISTRY_UPDATED, dispatcher_callable=_async_dispatch_device_id_event, filter_callable=_async_device_registry_updated_filter, - run_immediately=True, ) @@ -613,7 +605,6 @@ _KEYED_TRACK_STATE_ADDED_DOMAIN = _KeyedEventTracker( event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_domain_event, filter_callable=_async_domain_added_filter, - run_immediately=False, ) @@ -649,7 +640,6 @@ _KEYED_TRACK_STATE_REMOVED_DOMAIN = _KeyedEventTracker( event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_domain_event, filter_callable=_async_domain_removed_filter, - run_immediately=False, ) @@ -1635,16 +1625,16 @@ def async_track_time_interval( track_time_interval = threaded_listener_factory(async_track_time_interval) -@attr.s +@dataclass(slots=True) class SunListener: """Helper class to help listen to sun events.""" - hass: HomeAssistant = attr.ib() - job: HassJob[[], Coroutine[Any, Any, None] | None] = attr.ib() - event: str = attr.ib() - offset: timedelta | None = attr.ib() - _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None) - _unsub_config: CALLBACK_TYPE | None = attr.ib(default=None) + hass: HomeAssistant + job: HassJob[[], Coroutine[Any, Any, None] | None] + event: str + offset: timedelta | None + _unsub_sun: CALLBACK_TYPE | None = None + _unsub_config: CALLBACK_TYPE | None = None @callback def async_attach(self) -> None: diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index b168b81c1a9..4a11d85176a 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -5,10 +5,11 @@ from __future__ import annotations from collections.abc import Iterable import dataclasses from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal, TypedDict, cast +from typing import Literal, TypedDict, cast from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify +from homeassistant.util.event_type import EventType from .normalized_name_base_registry import ( NormalizedNameBaseRegistryEntry, @@ -20,11 +21,29 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "floor_registry" -EVENT_FLOOR_REGISTRY_UPDATED = "floor_registry_updated" +EVENT_FLOOR_REGISTRY_UPDATED: EventType[EventFloorRegistryUpdatedData] = EventType( + "floor_registry_updated" +) STORAGE_KEY = "core.floor_registry" STORAGE_VERSION_MAJOR = 1 +class _FloorStoreData(TypedDict): + """Data type for individual floor. Used in FloorRegistryStoreData.""" + + aliases: list[str] + floor_id: str + icon: str | None + level: int | None + name: str + + +class FloorRegistryStoreData(TypedDict): + """Store data type for FloorRegistry.""" + + floors: list[_FloorStoreData] + + class EventFloorRegistryUpdatedData(TypedDict): """Event data for when the floor registry is updated.""" @@ -45,7 +64,7 @@ class FloorEntry(NormalizedNameBaseRegistryEntry): level: int | None = None -class FloorRegistry(BaseRegistry): +class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): """Class to hold a registry of floors.""" floors: NormalizedNameBaseRegistryItems[FloorEntry] @@ -54,13 +73,11 @@ class FloorRegistry(BaseRegistry): 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( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, ) @callback @@ -190,13 +207,6 @@ class FloorRegistry(BaseRegistry): if data is not None: for floor in data["floors"]: - if TYPE_CHECKING: - assert isinstance(floor["aliases"], list) - assert isinstance(floor["icon"], str) - assert isinstance(floor["level"], int) - assert isinstance(floor["name"], str) - assert isinstance(floor["floor_id"], str) - normalized_name = normalize_name(floor["name"]) floors[floor["floor_id"]] = FloorEntry( aliases=set(floor["aliases"]), @@ -211,7 +221,7 @@ class FloorRegistry(BaseRegistry): self._floor_data = floors.data @callback - def _data_to_save(self) -> dict[str, list[dict[str, str | int | list[str] | None]]]: + def _data_to_save(self) -> FloorRegistryStoreData: """Return data of floor registry to store in a file.""" return { "floors": [ diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index ee092717753..068a12c0598 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -7,21 +7,17 @@ from collections.abc import Callable from contextlib import suppress from dataclasses import dataclass import functools +from functools import cached_property import linecache import logging import sys from types import FrameType -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import 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 @@ -140,6 +136,7 @@ def report( error_if_core: bool = True, level: int = logging.WARNING, log_custom_component_only: bool = False, + error_if_integration: bool = False, ) -> None: """Report incorrect usage. @@ -157,14 +154,19 @@ def report( _LOGGER.warning(msg, stack_info=True) return - if not log_custom_component_only or integration_frame.custom_integration: - _report_integration(what, integration_frame, level) + if ( + error_if_integration + or not log_custom_component_only + or integration_frame.custom_integration + ): + _report_integration(what, integration_frame, level, error_if_integration) def _report_integration( what: str, integration_frame: IntegrationFrame, level: int = logging.WARNING, + error: bool = False, ) -> None: """Report incorrect usage in an integration. @@ -172,7 +174,7 @@ def _report_integration( """ # Keep track of integrations already reported to prevent flooding key = f"{integration_frame.filename}:{integration_frame.line_number}" - if key in _REPORTED_INTEGRATIONS: + if not error and key in _REPORTED_INTEGRATIONS: return _REPORTED_INTEGRATIONS.add(key) @@ -184,11 +186,11 @@ def _report_integration( integration_domain=integration_frame.integration, module=integration_frame.module, ) - + integration_type = "custom " if integration_frame.custom_integration else "" _LOGGER.log( level, "Detected that %sintegration '%s' %s at %s, line %s: %s, please %s", - "custom " if integration_frame.custom_integration else "", + integration_type, integration_frame.integration, what, integration_frame.relative_filename, @@ -196,6 +198,15 @@ def _report_integration( integration_frame.line, report_issue, ) + if not error: + return + raise RuntimeError( + f"Detected that {integration_type}integration " + f"'{integration_frame.integration}' {what} at " + f"{integration_frame.relative_filename}, line " + f"{integration_frame.line_number}: {integration_frame.line}. " + f"Please {report_issue}." + ) def warn_use(func: _CallableT, what: str) -> _CallableT: diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index 2855705b9c1..a0112ae0843 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -57,7 +57,7 @@ class HassHttpXAsyncClient(httpx.AsyncClient): """Prevent an integration from reopen of the client via context manager.""" return self - async def __aexit__(self, *args: Any) -> None: + async def __aexit__(self, *args: object) -> None: """Prevent an integration from close of the client via context manager.""" diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 973c93674b1..db90d38744a 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Iterable from functools import lru_cache import logging +import pathlib from typing import Any from homeassistant.core import HomeAssistant, callback @@ -20,23 +21,17 @@ _LOGGER = logging.getLogger(__name__) @callback -def _component_icons_path(component: str, integration: Integration) -> str | None: +def _component_icons_path(integration: Integration) -> pathlib.Path: """Return the icons json file location for a component. Ex: components/hue/icons.json - If component is just a single file, will return None. """ - domain = component.rpartition(".")[-1] - - # If it's a component that is just one file, we don't support icons - # Example custom_components/my_component.py - if integration.file_path.name != domain: - return None - - return str(integration.file_path / "icons.json") + return integration.file_path / "icons.json" -def _load_icons_files(icons_files: dict[str, str]) -> dict[str, dict[str, Any]]: +def _load_icons_files( + icons_files: dict[str, pathlib.Path], +) -> dict[str, dict[str, Any]]: """Load and parse icons.json files.""" return { component: load_json_object(icons_file) @@ -53,19 +48,15 @@ async def _async_get_component_icons( icons: dict[str, Any] = {} # Determine files to load - files_to_load = {} - for loaded in components: - domain = loaded.rpartition(".")[-1] - if (path := _component_icons_path(loaded, integrations[domain])) is None: - icons[loaded] = {} - else: - files_to_load[loaded] = path + files_to_load = { + comp: _component_icons_path(integrations[comp]) for comp in components + } # Load files - if files_to_load and ( - load_icons_job := hass.async_add_executor_job(_load_icons_files, files_to_load) - ): - icons |= await load_icons_job + if files_to_load: + icons.update( + await hass.async_add_executor_job(_load_icons_files, files_to_load) + ) return icons @@ -108,8 +99,7 @@ class _IconsCache: _LOGGER.debug("Cache miss for: %s", components) integrations: dict[str, Integration] = {} - domains = {loaded.rpartition(".")[-1] for loaded in components} - ints_or_excs = await async_get_integrations(self._hass, domains) + ints_or_excs = await async_get_integrations(self._hass, components) for domain, int_or_exc in ints_or_excs.items(): if isinstance(int_or_exc, Exception): raise int_or_exc @@ -127,11 +117,9 @@ class _IconsCache: icons: dict[str, dict[str, Any]], ) -> None: """Extract resources into the cache.""" - categories: set[str] = set() - - for resource in icons.values(): - categories.update(resource) - + categories = { + category for component in icons.values() for category in component + } for category in categories: self._cache.setdefault(category, {}).update( build_resources(icons, components, category) @@ -151,9 +139,7 @@ async def async_get_icons( if integrations: components = set(integrations) else: - components = { - component for component in hass.config.components if "." not in component - } + components = hass.config.top_level_components if ICON_CACHE in hass.data: cache: _IconsCache = hass.data[ICON_CACHE] diff --git a/homeassistant/helpers/importlib.py b/homeassistant/helpers/importlib.py index 00af75f6d8e..98c75939084 100644 --- a/homeassistant/helpers/importlib.py +++ b/homeassistant/helpers/importlib.py @@ -30,11 +30,9 @@ async def async_import_module(hass: HomeAssistant, name: str) -> ModuleType: 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 + failure_cache: dict[str, bool] = hass.data.setdefault(DATA_IMPORT_FAILURES, {}) + if name in failure_cache: + raise ModuleNotFoundError(f"{name} not found", name=name) import_futures: dict[str, asyncio.Future[ModuleType]] import_futures = hass.data.setdefault(DATA_IMPORT_FUTURES, {}) @@ -51,7 +49,8 @@ async def async_import_module(hass: HomeAssistant, name: str) -> ModuleType: 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 + if isinstance(ex, ModuleNotFoundError): + failure_cache[name] = True import_future.set_exception(ex) with suppress(BaseException): # Set the exception retrieved flag on the future since diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 6d474557748..fbd26019b64 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -85,7 +85,7 @@ def _async_integration_platform_component_loaded( # 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( + hass.async_create_task_internal( _async_process_integration_platforms_for_component( hass, integration, platforms_that_exist, integration_platforms_by_name ), @@ -169,13 +169,12 @@ async def async_process_integration_platforms( 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} + top_level_components = hass.config.top_level_components.copy() process_job = HassJob( catch_log_exception( process_platform, @@ -207,7 +206,7 @@ async def async_process_integration_platforms( # 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( + future = hass.async_create_task_internal( _async_process_integration_platforms( hass, platform_name, top_level_components.copy(), process_job ), diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index fcebf91b854..119142ec14a 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from enum import Enum from functools import cached_property import logging -from typing import Any, TypeVar +from typing import Any import voluptuous as vol @@ -34,7 +34,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] -_T = TypeVar("_T") INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_ON = "HassTurnOn" @@ -115,7 +114,6 @@ async def async_handle( try: _LOGGER.info("Triggering intent handler %s", handler) result = await handler.async_handle(intent) - return result except vol.Invalid as err: _LOGGER.warning("Received invalid slot info for %s: %s", intent_type, err) raise InvalidSlotInfo(f"Received invalid slot info for {intent_type}") from err @@ -123,6 +121,7 @@ async def async_handle( raise # bubble up intent related errors except Exception as err: raise IntentUnexpectedError(f"Error handling {intent_type}") from err + return result class IntentError(HomeAssistantError): @@ -611,7 +610,9 @@ class DynamicServiceIntentHandler(IntentHandler): # Handle service calls in parallel, noting failures as they occur. failed_results: list[IntentResponseTarget] = [] - for state, service_coro in zip(states, asyncio.as_completed(service_coros)): + for state, service_coro in zip( + states, asyncio.as_completed(service_coros), strict=False + ): target = IntentResponseTarget( type=IntentResponseTargetType.ENTITY, name=state.name, @@ -658,7 +659,7 @@ class DynamicServiceIntentHandler(IntentHandler): ) await self._run_then_background( - hass.async_create_task( + hass.async_create_task_internal( hass.services.async_call( domain, service, diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index b3ca89140a1..81901c71745 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -9,6 +9,7 @@ from typing import Literal, TypedDict, cast from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify +from homeassistant.util.event_type import EventType from .normalized_name_base_registry import ( NormalizedNameBaseRegistryEntry, @@ -20,11 +21,29 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "label_registry" -EVENT_LABEL_REGISTRY_UPDATED = "label_registry_updated" +EVENT_LABEL_REGISTRY_UPDATED: EventType[EventLabelRegistryUpdatedData] = EventType( + "label_registry_updated" +) STORAGE_KEY = "core.label_registry" STORAGE_VERSION_MAJOR = 1 +class _LabelStoreData(TypedDict): + """Data type for individual label. Used in LabelRegistryStoreData.""" + + color: str | None + description: str | None + icon: str | None + label_id: str + name: str + + +class LabelRegistryStoreData(TypedDict): + """Store data type for LabelRegistry.""" + + labels: list[_LabelStoreData] + + class EventLabelRegistryUpdatedData(TypedDict): """Event data for when the label registry is updated.""" @@ -45,7 +64,7 @@ class LabelEntry(NormalizedNameBaseRegistryEntry): icon: str | None = None -class LabelRegistry(BaseRegistry): +class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): """Class to hold a registry of labels.""" labels: NormalizedNameBaseRegistryItems[LabelEntry] @@ -54,7 +73,7 @@ class LabelRegistry(BaseRegistry): def __init__(self, hass: HomeAssistant) -> None: """Initialize the label registry.""" self.hass = hass - self._store: Store[dict[str, list[dict[str, str | None]]]] = Store( + self._store = Store( hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, @@ -189,10 +208,6 @@ class LabelRegistry(BaseRegistry): if data is not None: for label in data["labels"]: - # Check if the necessary keys are present - if label["label_id"] is None or label["name"] is None: - continue - normalized_name = normalize_name(label["name"]) labels[label["label_id"]] = LabelEntry( color=label["color"], @@ -207,7 +222,7 @@ class LabelRegistry(BaseRegistry): self._label_data = labels.data @callback - def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + def _data_to_save(self) -> LabelRegistryStoreData: """Return data of label registry to store in a file.""" return { "labels": [ diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index ed6339f9996..d5891973e40 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -31,9 +31,9 @@ def is_internal_request(hass: HomeAssistant) -> bool: get_url( hass, allow_external=False, allow_cloud=False, require_current_request=True ) - return True except NoURLAvailableError: return False + return True @bind_hass @@ -122,6 +122,7 @@ def get_url( require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, + require_cloud: bool = False, allow_internal: bool = True, allow_external: bool = True, allow_cloud: bool = True, @@ -145,7 +146,7 @@ def get_url( # Try finding an URL in the order specified for url_type in order: - if allow_internal and url_type == TYPE_URL_INTERNAL: + if allow_internal and url_type == TYPE_URL_INTERNAL and not require_cloud: with suppress(NoURLAvailableError): return _get_internal_url( hass, @@ -155,7 +156,7 @@ def get_url( require_standard_port=require_standard_port, ) - if allow_external and url_type == TYPE_URL_EXTERNAL: + if require_cloud or (allow_external and url_type == TYPE_URL_EXTERNAL): with suppress(NoURLAvailableError): return _get_external_url( hass, @@ -165,7 +166,10 @@ def get_url( require_current_request=require_current_request, require_ssl=require_ssl, require_standard_port=require_standard_port, + require_cloud=require_cloud, ) + if require_cloud: + raise NoURLAvailableError # For current request, we accept loopback interfaces (e.g., 127.0.0.1), # the Supervisor hostname and localhost transparently @@ -263,8 +267,12 @@ def _get_external_url( require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, + require_cloud: bool = False, ) -> str: """Get external URL of this instance.""" + if require_cloud: + return _get_cloud_url(hass, require_current_request=require_current_request) + if prefer_cloud and allow_cloud: with suppress(NoURLAvailableError): return _get_cloud_url(hass) diff --git a/homeassistant/helpers/normalized_name_base_registry.py b/homeassistant/helpers/normalized_name_base_registry.py index 16280a73750..f14d99b7831 100644 --- a/homeassistant/helpers/normalized_name_base_registry.py +++ b/homeassistant/helpers/normalized_name_base_registry.py @@ -1,10 +1,11 @@ """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 functools import lru_cache from typing import TypeVar +from .registry import BaseRegistryItems + @dataclass(slots=True, frozen=True, kw_only=True) class NormalizedNameBaseRegistryEntry: @@ -17,12 +18,13 @@ class NormalizedNameBaseRegistryEntry: _VT = TypeVar("_VT", bound=NormalizedNameBaseRegistryEntry) +@lru_cache(maxsize=1024) def normalize_name(name: str) -> str: """Normalize a name by removing whitespace and case folding.""" return name.casefold().replace(" ", "") -class NormalizedNameBaseRegistryItems(UserDict[str, _VT]): +class NormalizedNameBaseRegistryItems(BaseRegistryItems[_VT]): """Base container for normalized name registry items, maps key -> entry. Maintains an additional index: @@ -34,34 +36,21 @@ class NormalizedNameBaseRegistryItems(UserDict[str, _VT]): 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 _unindex_entry(self, key: str, replacement_entry: _VT | None = None) -> None: + old_entry = self.data[key] + if ( + replacement_entry is not None + and (normalized_name := normalize_name(replacement_entry.name)) + != old_entry.normalized_name + and normalized_name in self._normalized_names + ): + raise ValueError( + f"The name {replacement_entry.name} ({normalized_name}) is already in use" + ) + del self._normalized_names[old_entry.normalized_name] - 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 _index_entry(self, key: str, entry: _VT) -> None: + self._normalized_names[normalize_name(entry.name)] = entry def get_by_name(self, name: str) -> _VT | None: """Get entry by name.""" diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index 516d4134f76..020c7c3a0d3 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -30,7 +30,7 @@ class KeyedRateLimit: @callback def async_has_timer(self, key: Hashable) -> bool: """Check if a rate limit timer is running.""" - return bool(self._rate_limit_timers and key in self._rate_limit_timers) + return key in self._rate_limit_timers @callback def async_triggered(self, key: Hashable, now: float | None = None) -> None: @@ -41,10 +41,8 @@ class KeyedRateLimit: @callback def async_cancel_timer(self, key: Hashable) -> None: """Cancel a rate limit time that will call the action.""" - if not self._rate_limit_timers or key not in self._rate_limit_timers: - return - - self._rate_limit_timers.pop(key).cancel() + if handle := self._rate_limit_timers.pop(key, None): + handle.cancel() @callback def async_remove(self) -> None: diff --git a/homeassistant/helpers/registry.py b/homeassistant/helpers/registry.py index d5b1035531a..832f50661ae 100644 --- a/homeassistant/helpers/registry.py +++ b/homeassistant/helpers/registry.py @@ -3,7 +3,9 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any +from collections import UserDict +from collections.abc import Mapping, Sequence, ValuesView +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar from homeassistant.core import CoreState, HomeAssistant, callback @@ -14,11 +16,60 @@ SAVE_DELAY = 10 SAVE_DELAY_LONG = 180 -class BaseRegistry(ABC): +_DataT = TypeVar("_DataT") +_StoreDataT = TypeVar("_StoreDataT", bound=Mapping[str, Any] | Sequence[Any]) + + +class BaseRegistryItems(UserDict[str, _DataT], ABC): + """Base class for registry items.""" + + data: dict[str, _DataT] + + def values(self) -> ValuesView[_DataT]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + + @abstractmethod + def _index_entry(self, key: str, entry: _DataT) -> None: + """Index an entry.""" + + @abstractmethod + def _unindex_entry(self, key: str, replacement_entry: _DataT | None = None) -> None: + """Unindex an entry.""" + + def __setitem__(self, key: str, entry: _DataT) -> None: + """Add an item.""" + data = self.data + if key in data: + self._unindex_entry(key, entry) + data[key] = entry + self._index_entry(key, entry) + + def _unindex_entry_value( + self, key: str, value: str, index: dict[str, dict[str, Literal[True]]] + ) -> None: + """Unindex an entry value. + + key is the entry key + value is the value to unindex such as config_entry_id or device_id. + index is the index to unindex from. + """ + entries = index[value] + del entries[key] + if not entries: + del index[value] + + def __delitem__(self, key: str) -> None: + """Remove an item.""" + self._unindex_entry(key) + super().__delitem__(key) + + +class BaseRegistry(ABC, Generic[_StoreDataT]): """Class to implement a registry.""" hass: HomeAssistant - _store: Store + _store: Store[_StoreDataT] @callback def async_schedule_save(self) -> None: @@ -30,5 +81,5 @@ class BaseRegistry(ABC): @callback @abstractmethod - def _data_to_save(self) -> dict[str, Any]: + def _data_to_save(self) -> _StoreDataT: """Return data of registry to store in a file.""" diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index ffd6bdeb50d..cdd53731d6e 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -156,7 +156,7 @@ async def async_integration_yaml_config( hass: HomeAssistant, integration_name: str, *, - raise_on_failure: Literal[False] | bool, + raise_on_failure: Literal[False], ) -> ConfigType | None: ... diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 7979247c8b0..2b3afc2f57b 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -73,12 +73,11 @@ class StoredState: def as_dict(self) -> dict[str, Any]: """Return a dict representation of the stored state to be JSON serialized.""" - result = { + return { "state": self.state.json_fragment, "extra_data": self.extra_data.as_dict() if self.extra_data else None, "last_seen": self.last_seen, } - return result @classmethod def from_dict(cls, json_dict: dict) -> Self: @@ -237,7 +236,9 @@ class RestoreStateData: # Dump the initial states now. This helps minimize the risk of having # old states loaded by overwriting the last states once Home Assistant # has started and the old states have been read. - self.hass.async_create_task(_async_dump_states(), "RestoreStateData dump") + self.hass.async_create_task_internal( + _async_dump_states(), "RestoreStateData dump" + ) # Dump states periodically cancel_interval = async_track_time_interval( @@ -253,7 +254,7 @@ class RestoreStateData: # Dump states when stopping hass self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop, run_immediately=True + EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop ) @callback diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 0486c7b6f8c..67624bfb368 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Callable, Coroutine, Mapping +from collections.abc import Callable, Container, Coroutine, Mapping import copy from dataclasses import dataclass import types @@ -102,7 +102,7 @@ class SchemaFlowMenuStep(SchemaFlowStep): """Define a config or options flow menu step.""" # Menu options - options: list[str] | dict[str, str] + options: Container[str] class SchemaCommonFlowHandler: @@ -357,8 +357,7 @@ class SchemaConfigFlowHandler(ConfigFlow, ABC): ) -> ConfigFlowResult: """Handle a config flow step.""" # pylint: disable-next=protected-access - result = await self._common_handler.async_step(step_id, user_input) - return result + return await self._common_handler.async_step(step_id, user_input) return _async_step @@ -452,8 +451,7 @@ class SchemaOptionsFlowHandler(OptionsFlowWithConfigEntry): ) -> ConfigFlowResult: """Handle an options flow step.""" # pylint: disable-next=protected-access - result = await self._common_handler.async_step(step_id, user_input) - return result + return await self._common_handler.async_step(step_id, user_input) return _async_step diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 2b0eb90827e..1bbe7749ff7 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,11 +9,11 @@ from contextvars import ContextVar from copy import copy from dataclasses import dataclass from datetime import datetime, timedelta -from functools import partial +from functools import cached_property, partial import itertools import logging from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast +from typing import Any, Literal, TypedDict, TypeVar, cast import async_interrupt import voluptuous as vol @@ -107,12 +107,6 @@ from .trace import ( from .trigger import async_initialize_triggers, async_validate_trigger_config from .typing import UNDEFINED, ConfigType, UndefinedType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _T = TypeVar("_T") @@ -240,16 +234,16 @@ async def trace_action( yield trace_element except _AbortScript as ex: trace_element.set_error(ex.__cause__ or ex) - raise ex - except _ConditionFail as ex: + raise + except _ConditionFail: # Clear errors which may have been set when evaluating the condition trace_element.set_error(None) - raise ex - except _StopScript as ex: - raise ex + raise + except _StopScript: + raise except Exception as ex: trace_element.set_error(ex) - raise ex + raise finally: trace_stack_pop(trace_stack_cv) @@ -472,7 +466,7 @@ class _ScriptRun: if not self._script.top_level: # We already consumed the response, do not pass it on err.response = None - raise err + raise except Exception: script_execution_set("error") raise @@ -740,7 +734,7 @@ class _ScriptRun: ) trace_set_result(params=params, running_script=running_script) response_data = await self._async_run_long_action( - self._hass.async_create_task( + self._hass.async_create_task_internal( self._hass.services.async_call( **params, blocking=True, @@ -790,7 +784,7 @@ class _ScriptRun: ) trace_set_result(event=self._action[CONF_EVENT], event_data=event_data) - self._hass.bus.async_fire( + self._hass.bus.async_fire_internal( self._action[CONF_EVENT], event_data, context=self._context ) @@ -832,8 +826,7 @@ class _ScriptRun: return True - result = traced_test_conditions(self._hass, self._variables) - return result + return traced_test_conditions(self._hass, self._variables) @async_trace_path("repeat") async def _async_repeat_step(self): # noqa: C901 @@ -1215,7 +1208,7 @@ class _ScriptRun: async def _async_run_script(self, script: Script) -> None: """Execute a script.""" result = await self._async_run_long_action( - self._hass.async_create_task( + self._hass.async_create_task_internal( script.async_run(self._variables, self._context), eager_start=True ) ) @@ -1272,7 +1265,7 @@ async def _async_stop_scripts_after_shutdown( _LOGGER.warning("Stopping scripts running too long after shutdown: %s", names) await asyncio.gather( *( - script["instance"].async_stop(update_state=False) + create_eager_task(script["instance"].async_stop(update_state=False)) for script in running_scripts ) ) @@ -1291,7 +1284,10 @@ async def _async_stop_scripts_at_shutdown(hass: HomeAssistant, event: Event) -> names = ", ".join([script["instance"].name for script in running_scripts]) _LOGGER.debug("Stopping scripts running at shutdown: %s", names) await asyncio.gather( - *(script["instance"].async_stop() for script in running_scripts) + *( + create_eager_task(script["instance"].async_stop()) + for script in running_scripts + ) ) @@ -1696,7 +1692,7 @@ class Script: script_stack = script_stack_cv.get() if ( self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED) - and (script_stack := script_stack_cv.get()) is not None + and script_stack is not None and id(self) in script_stack ): script_execution_set("disallowed_recursion_detected") @@ -1710,13 +1706,20 @@ class Script: run = cls( self._hass, self, cast(dict, variables), context, self._log_exceptions ) + has_existing_runs = bool(self._runs) self._runs.append(run) - if self.script_mode == SCRIPT_MODE_RESTART: + if self.script_mode == SCRIPT_MODE_RESTART and has_existing_runs: # When script mode is SCRIPT_MODE_RESTART, first add the new run and then # stop any other runs. If we stop other runs first, self.is_running will # return false after the other script runs were stopped until our task - # resumes running. + # resumes running. Its important that we check if there are existing + # runs before sleeping as otherwise if two runs are started at the exact + # same time they will cancel each other out. self._log("Restarting") + # Important: yield to the event loop to allow the script to start in case + # the script is restarting itself so it ends up in the script stack and + # the recursion check above will prevent the script from running. + await asyncio.sleep(0) await self.async_stop(update_state=False, spare=run) if started_action: @@ -1731,9 +1734,7 @@ class Script: self._changed() raise - async def _async_stop( - self, aws: list[asyncio.Task], update_state: bool, spare: _ScriptRun | None - ) -> None: + async def _async_stop(self, aws: list[asyncio.Task], update_state: bool) -> None: await asyncio.wait(aws) if update_state: self._changed() @@ -1746,11 +1747,11 @@ class Script: # asyncio.shield as asyncio.shield yields to the event loop, which would cause # us to wait for script runs added after the call to async_stop. aws = [ - asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + create_eager_task(run.async_stop()) for run in self._runs if run != spare ] if not aws: return - await asyncio.shield(self._async_stop(aws, update_state, spare)) + await asyncio.shield(create_eager_task(self._async_stop(aws, update_state))) async def _async_get_condition(self, config): if isinstance(config, template.Template): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index da27df9d139..66c9f7db3e6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -77,6 +77,8 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" ALL_SERVICE_DESCRIPTIONS_CACHE = "all_service_descriptions_cache" +_T = TypeVar("_T") + @cache def _base_components() -> dict[str, ModuleType]: @@ -93,6 +95,7 @@ def _base_components() -> dict[str, ModuleType]: light, lock, media_player, + notify, remote, siren, todo, @@ -112,6 +115,7 @@ def _base_components() -> dict[str, ModuleType]: "light": light, "lock": lock, "media_player": media_player, + "notify": notify, "remote": remote, "siren": siren, "todo": todo, @@ -503,15 +507,15 @@ def async_extract_referenced_entity_ids( # noqa: C901 ): return selected - ent_reg = entity_registry.async_get(hass) + entities = entity_registry.async_get(hass).entities 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 floor_id in selector.floor_ids: - if floor_id not in floor_reg.floors: - selected.missing_floors.add(floor_id) + if selector.floor_ids: + floor_reg = floor_registry.async_get(hass) + 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: @@ -521,47 +525,47 @@ def async_extract_referenced_entity_ids( # noqa: C901 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) + label_reg = label_registry.async_get(hass) + for label_id in selector.label_ids: + if label_id not in label_reg.labels: + selected.missing_labels.add(label_id) - for device_entry in dev_reg.devices.values(): - if device_entry.labels.intersection(selector.label_ids): + for entity_entry in entities.get_entries_for_label(label_id): + if ( + entity_entry.entity_category is None + and entity_entry.hidden_by is None + ): + selected.indirectly_referenced.add(entity_entry.entity_id) + + for device_entry in dev_reg.devices.get_devices_for_label(label_id): 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) + for area_entry in area_reg.areas.get_areas_for_label(label_id): + selected.referenced_areas.add(area_entry.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) + selected.referenced_areas.update( + area_entry.id + for floor_id in selector.floor_ids + for area_entry in area_reg.areas.get_areas_for_floor(floor_id) + ) # Find devices for targeted areas selected.referenced_devices.update(selector.device_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 selected.referenced_areas: - selected.referenced_devices.add(device_entry.id) + for area_id in selected.referenced_areas: + selected.referenced_devices.update( + device_entry.id + for device_entry in dev_reg.devices.get_devices_for_area_id(area_id) + ) 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 @@ -707,7 +711,7 @@ async def async_get_all_descriptions( contents = await hass.async_add_executor_job( _load_services_files, hass, integrations ) - loaded = dict(zip(domains_with_missing_services, contents)) + loaded = dict(zip(domains_with_missing_services, contents, strict=False)) # Load translations for all service domains translations = await translation.async_get_translations( @@ -991,7 +995,7 @@ async def entity_service_call( ) response_data: EntityServiceResponse = {} - for entity, result in zip(entities, results): + for entity, result in zip(entities, results, strict=False): if isinstance(result, BaseException): raise result from None response_data[entity.entity_id] = result @@ -1152,40 +1156,67 @@ def verify_domain_control( class ReloadServiceHelper: - """Helper for reload services to minimize unnecessary reloads.""" + """Helper for reload services. - def __init__(self, service_func: Callable[[ServiceCall], Awaitable]) -> None: + The helper has the following purposes: + - Make sure reloads do not happen in parallel + - Avoid redundant reloads of the same target + """ + + def __init__( + self, + service_func: Callable[[ServiceCall], Awaitable], + reload_targets_func: Callable[[ServiceCall], set[_T]], + ) -> None: """Initialize ReloadServiceHelper.""" self._service_func = service_func self._service_running = False self._service_condition = asyncio.Condition() + self._pending_reload_targets: set[_T] = set() + self._reload_targets_func = reload_targets_func async def execute_service(self, service_call: ServiceCall) -> None: """Execute the service. - If a previous reload task if currently in progress, wait for it to finish first. + If a previous reload task is currently in progress, wait for it to finish first. Once the previous reload task has finished, one of the waiting tasks will be - assigned to execute the reload, the others will wait for the reload to finish. + assigned to execute the reload of the targets it is assigned to reload. The + other tasks will wait if they should reload the same target, otherwise they + will wait for the next round. """ do_reload = False + reload_targets = None async with self._service_condition: if self._service_running: - # A previous reload task is already in progress, wait for it to finish + # A previous reload task is already in progress, wait for it to finish, + # because that task may be reloading a stale version of the resource. await self._service_condition.wait() - async with self._service_condition: - if not self._service_running: - # This task will do the reload - self._service_running = True - do_reload = True - else: - # Another task will perform the reload, wait for it to finish + while True: + async with self._service_condition: + # Once we've passed this point, we assume the version of the resource is + # the one our task was assigned to reload, or a newer one. Regardless of + # which, our task is happy as long as the target is reloaded at least + # once. + if reload_targets is None: + reload_targets = self._reload_targets_func(service_call) + self._pending_reload_targets |= reload_targets + if not self._service_running: + # This task will do a reload + self._service_running = True + do_reload = True + break + # Another task will perform a reload, wait for it to finish await self._service_condition.wait() + # Check if the reload this task is waiting for has been completed + if reload_targets.isdisjoint(self._pending_reload_targets): + break if do_reload: # Reload, then notify other tasks await self._service_func(service_call) async with self._service_condition: self._service_running = False + self._pending_reload_targets -= reload_targets self._service_condition.notify_all() diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index 44b103e5c27..1b1f1b5c617 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -99,9 +99,9 @@ def either_one_none(val1: Any | None, val2: Any | None) -> bool: def _check_numeric_change( - old_state: int | float | None, - new_state: int | float | None, - change: int | float, + old_state: float | None, + new_state: float | None, + change: float, metric: Callable[[int | float, int | float], int | float], ) -> bool: """Check if two numeric values have changed.""" @@ -121,9 +121,9 @@ def _check_numeric_change( def check_absolute_change( - val1: int | float | None, - val2: int | float | None, - change: int | float, + val1: float | None, + val2: float | None, + change: float, ) -> bool: """Check if two numeric values have changed.""" return _check_numeric_change( @@ -132,13 +132,13 @@ def check_absolute_change( def check_percentage_change( - old_state: int | float | None, - new_state: int | float | None, - change: int | float, + old_state: float | None, + new_state: float | None, + change: float, ) -> bool: """Check if two numeric values have changed.""" - def percentage_change(old_state: int | float, new_state: int | float) -> float: + def percentage_change(old_state: float, new_state: float) -> float: if old_state == new_state: return 0 try: @@ -149,7 +149,7 @@ def check_percentage_change( return _check_numeric_change(old_state, new_state, change, percentage_change) -def check_valid_float(value: str | int | float) -> bool: +def check_valid_float(value: str | float) -> bool: """Check if given value is a valid float.""" try: float(value) diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 148b416e087..70664430582 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -14,13 +14,16 @@ from homeassistant.core import ( HomeAssistant, callback, ) +from homeassistant.util.event_type import EventType + +from .typing import NoEventData @callback def _async_at_core_state( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], - event_type: str, + event_type: EventType[NoEventData], check_state: Callable[[HomeAssistant], bool], ) -> CALLBACK_TYPE: """Execute a job at_start_cb when Home Assistant has the wanted state. diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 92a31ae9345..315d28e06e6 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,12 +6,13 @@ import asyncio from collections.abc import Callable, Iterable, Mapping, Sequence from contextlib import suppress from copy import deepcopy +from functools import cached_property import inspect from json import JSONDecodeError, JSONEncoder import logging import os from pathlib import Path -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import Any, Generic, TypeVar from homeassistant.const import ( EVENT_HOMEASSISTANT_FINAL_WRITE, @@ -34,12 +35,6 @@ from homeassistant.util.file import WriteError from . import json as json_helper -if TYPE_CHECKING: - from functools import cached_property -else: - from ..backports.functools import cached_property - - # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs MAX_LOAD_CONCURRENTLY = 6 @@ -130,7 +125,6 @@ class _StoreManager: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._async_schedule_cleanup, - run_immediately=True, ) @callback @@ -190,7 +184,6 @@ class _StoreManager: self._hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_cancel_and_cleanup, - run_immediately=True, ) @callback @@ -475,7 +468,7 @@ class Store(Generic[_T]): # wrote. Reschedule the timer to the next write time. self._async_reschedule_delayed_write(self._next_write_time) return - self.hass.async_create_task( + self.hass.async_create_task_internal( self._async_callback_delayed_write(), eager_start=True ) @@ -484,7 +477,8 @@ class Store(Generic[_T]): """Ensure that we write if we quit before delay has passed.""" if self._unsub_final_write_listener is None: self._unsub_final_write_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write + EVENT_HOMEASSISTANT_FINAL_WRITE, + self._async_callback_final_write, ) @callback diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 5f692e0de89..c12494ba71b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -9,7 +9,7 @@ import collections.abc from collections.abc import Callable, Generator, Iterable from contextlib import AbstractContextManager, suppress from contextvars import ContextVar -from datetime import datetime, timedelta +from datetime import date, datetime, time, timedelta from functools import cache, lru_cache, partial, wraps import json import logging @@ -28,6 +28,7 @@ from typing import ( Literal, NoReturn, ParamSpec, + Self, TypeVar, cast, overload, @@ -211,12 +212,8 @@ 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, run_immediately=True - ) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, callback(lambda _: cancel()), run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_adjust_lru_sizes) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, callback(lambda _: cancel())) return True @@ -308,9 +305,11 @@ def gen_result_wrapper(kls: type[dict | list | set]) -> type: class TupleWrapper(tuple, ResultWrapper): """Wrap a tuple.""" + __slots__ = () + # This is all magic to be allowed to subclass a tuple. - def __new__(cls, value: tuple, *, render_result: str | None = None) -> TupleWrapper: + def __new__(cls, value: tuple, *, render_result: str | None = None) -> Self: """Create a new tuple class.""" return super().__new__(cls, tuple(value)) @@ -696,6 +695,8 @@ class Template: **kwargs: Any, ) -> RenderInfo: """Render the template and collect an entity filter.""" + if self.hass and self.hass.config.debug: + self.hass.verify_event_loop_thread("async_render_to_info") self._renders += 1 assert self.hass and _render_info.get() is None @@ -1102,7 +1103,7 @@ class TemplateStateBase(State): return f"{state} {unit}" return state - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Ensure we collect on equality check.""" self._collect_state() return self._state.__eq__(other) @@ -1348,8 +1349,8 @@ def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None: dev_reg = device_registry.async_get(hass) return next( ( - id - for id, device in dev_reg.devices.items() + device_id + for device_id, device in dev_reg.devices.items() if (name := device.name_by_user or device.name) and (str(entity_id_or_device_name) == name) ), @@ -1454,8 +1455,7 @@ def floor_areas(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: def areas(hass: HomeAssistant) -> Iterable[str | None]: """Return all areas.""" - area_reg = area_registry.async_get(hass) - return [area.id for area in area_reg.async_list_areas()] + return list(area_registry.async_get(hass).areas) def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: @@ -1581,7 +1581,7 @@ 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()] + return list(label_reg.labels) ent_reg = entity_registry.async_get(hass) @@ -2003,12 +2003,12 @@ def square_root(value, default=_SENTINEL): def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True, default=_SENTINEL): """Filter to convert given timestamp to format.""" try: - date = dt_util.utc_from_timestamp(value) + result = dt_util.utc_from_timestamp(value) if local: - date = dt_util.as_local(date) + result = dt_util.as_local(result) - return date.strftime(date_format) + return result.strftime(date_format) except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: @@ -2050,6 +2050,12 @@ def forgiving_as_timestamp(value, default=_SENTINEL): def as_datetime(value: Any, default: Any = _SENTINEL) -> Any: """Filter and to convert a time string or UNIX timestamp to datetime object.""" + # Return datetime.datetime object without changes + if type(value) is datetime: + return value + # Add midnight to datetime.date object + if type(value) is date: + return datetime.combine(value, time(0, 0, 0)) try: # Check for a valid UNIX timestamp string, int or float timestamp = float(value) @@ -2470,10 +2476,15 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will be returned. - Make sure date is not in the future, or else it will return None. + If the input datetime is in the future, + the input datetime will be returned. If the input are not a datetime object the input will be returned unmodified. + + Note: This template function is deprecated in favor of `time_until`, but is still + supported so as not to break old templates. """ + if (render_info := _render_info.get()) is not None: render_info.has_time = True @@ -2486,6 +2497,50 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: return dt_util.get_age(value) +def time_since(hass: HomeAssistant, value: Any | datetime, precision: int = 1) -> Any: + """Take a datetime and return its "age" as a string. + + The age can be in seconds, minutes, hours, days, months and year. + + precision is the number of units to return, with the last unit rounded. + + If the value not a datetime object the input will be returned unmodified. + """ + if (render_info := _render_info.get()) is not None: + render_info.has_time = True + + if not isinstance(value, datetime): + return value + if not value.tzinfo: + value = dt_util.as_local(value) + if dt_util.now() < value: + return value + + return dt_util.get_age(value, precision) + + +def time_until(hass: HomeAssistant, value: Any | datetime, precision: int = 1) -> Any: + """Take a datetime and return the amount of time until that time as a string. + + The time until can be in seconds, minutes, hours, days, months and years. + + precision is the number of units to return, with the last unit rounded. + + If the value not a datetime object the input will be returned unmodified. + """ + if (render_info := _render_info.get()) is not None: + render_info.has_time = True + + if not isinstance(value, datetime): + return value + if not value.tzinfo: + value = dt_util.as_local(value) + if dt_util.now() > value: + return value + + return dt_util.get_time_remaining(value, precision) + + def urlencode(value): """Urlencode dictionary and return as UTF-8 string.""" return urllib_urlencode(value).encode("utf-8") @@ -2575,9 +2630,9 @@ def make_logging_undefined( def _fail_with_undefined_error(self, *args, **kwargs): try: return super()._fail_with_undefined_error(*args, **kwargs) - except self._undefined_exception as ex: + except self._undefined_exception: _log_fn(logging.ERROR, self._undefined_message) - raise ex + raise def __str__(self) -> str: """Log undefined __str___.""" @@ -2884,6 +2939,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "floor_id", "floor_name", "relative_time", + "time_since", + "time_until", "today_at", "label_id", "label_name", @@ -2940,6 +2997,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["now"] = hassfunction(now) self.globals["relative_time"] = hassfunction(relative_time) self.filters["relative_time"] = self.globals["relative_time"] + self.globals["time_since"] = hassfunction(time_since) + self.filters["time_since"] = self.globals["time_since"] + self.globals["time_until"] = hassfunction(time_until) + self.filters["time_until"] = self.globals["time_until"] self.globals["today_at"] = hassfunction(today_at) self.filters["today_at"] = self.globals["today_at"] diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index acc4f146e8b..377826b7edb 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Iterable, Mapping from contextlib import suppress import logging +import pathlib import string from typing import Any @@ -29,9 +30,11 @@ TRANSLATION_FLATTEN_CACHE = "translation_flatten_cache" LOCALE_EN = "en" -def recursive_flatten(prefix: Any, data: dict[str, Any]) -> dict[str, Any]: +def recursive_flatten( + prefix: str, data: dict[str, dict[str, Any] | str] +) -> dict[str, str]: """Return a flattened representation of dict data.""" - output = {} + output: dict[str, str] = {} for key, value in data.items(): if isinstance(value, dict): output.update(recursive_flatten(f"{prefix}{key}.", value)) @@ -41,40 +44,18 @@ def recursive_flatten(prefix: Any, data: dict[str, Any]) -> dict[str, Any]: @callback -def component_translation_path( - component: str, language: str, integration: Integration -) -> str | None: +def component_translation_path(language: str, integration: Integration) -> pathlib.Path: """Return the translation json file location for a component. For component: - components/hue/translations/nl.json - For platform: - - components/hue/translations/light.nl.json - - If component is just a single file, will return None. """ - parts = component.split(".") - domain = parts[0] - is_platform = len(parts) == 2 - - # If it's a component that is just one file, we don't support translations - # Example custom_components/my_component.py - if integration.file_path.name != domain: - return None - - if is_platform: - filename = f"{parts[1]}.{language}.json" - else: - filename = f"{language}.json" - - translation_path = integration.file_path / "translations" - - return str(translation_path / filename) + return integration.file_path / "translations" / f"{language}.json" def _load_translations_files_by_language( - translation_files: dict[str, dict[str, str]], + translation_files: dict[str, dict[str, pathlib.Path]], ) -> dict[str, dict[str, Any]]: """Load and parse translation.json files.""" loaded: dict[str, dict[str, Any]] = {} @@ -98,47 +79,6 @@ def _load_translations_files_by_language( return loaded -def _merge_resources( - translation_strings: dict[str, dict[str, Any]], - components: set[str], - category: str, -) -> dict[str, dict[str, Any]]: - """Build and merge the resources response for the given components and platforms.""" - # Build response - resources: dict[str, dict[str, Any]] = {} - for component in components: - domain = component.rpartition(".")[-1] - - domain_resources = resources.setdefault(domain, {}) - - # Integrations are able to provide translations for their entities under other - # integrations if they don't have an existing device class. This is done by - # using a custom device class prefixed with their domain and two underscores. - # These files are in platform specific files in the integration folder with - # names like `strings.sensor.json`. - # We are going to merge the translations for the custom device classes into - # the translations of sensor. - - new_value = translation_strings.get(component, {}).get(category) - - if new_value is None: - continue - - if isinstance(new_value, dict): - domain_resources.update(new_value) - else: - _LOGGER.error( - ( - "An integration providing translations for %s provided invalid" - " data: %s" - ), - domain, - new_value, - ) - - return resources - - def build_resources( translation_strings: dict[str, dict[str, dict[str, Any] | str]], components: set[str], @@ -163,32 +103,20 @@ async def _async_get_component_strings( """Load translations.""" translations_by_language: dict[str, dict[str, Any]] = {} # Determine paths of missing components/platforms - files_to_load_by_language: dict[str, dict[str, str]] = {} + files_to_load_by_language: dict[str, dict[str, pathlib.Path]] = {} 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] = {} - - for comp in components: - domain, _, platform = comp.partition(".") + files_to_load: dict[str, pathlib.Path] = { + domain: component_translation_path(language, integration) + for domain in components if ( - not (integration := integrations.get(domain)) - or not integration.has_translations - ): - continue - - 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 path := component_translation_path(comp, language, integration): - files_to_load[comp] = path - has_files_to_load = True + (integration := integrations.get(domain)) + and integration.has_translations + ) + } + files_to_load_by_language[language] = files_to_load + has_files_to_load |= bool(files_to_load) if has_files_to_load: loaded_translations_by_language = await hass.async_add_executor_job( @@ -197,18 +125,15 @@ async def _async_get_component_strings( for language in languages: loaded_translations = loaded_translations_by_language.setdefault(language, {}) - for comp in components: - if "." in comp: - continue - + for domain in components: # Translations that miss "title" will get integration put in. - component_translations = loaded_translations.setdefault(comp, {}) + component_translations = loaded_translations.setdefault(domain, {}) if "title" not in component_translations and ( - integration := integrations.get(comp) + integration := integrations.get(domain) ): component_translations["title"] = integration.name - translations_by_language[language].update(loaded_translations) + translations_by_language.setdefault(language, {}).update(loaded_translations) return translations_by_language @@ -289,8 +214,7 @@ class _TranslationCache: languages = [LOCALE_EN] if language == LOCALE_EN else [LOCALE_EN, language] integrations: dict[str, Integration] = {} - domains = {loaded.partition(".")[0] for loaded in components} - ints_or_excs = await async_get_integrations(self.hass, domains) + ints_or_excs = await async_get_integrations(self.hass, components) for domain, int_or_exc in ints_or_excs.items(): if isinstance(int_or_exc, Exception): _LOGGER.warning( @@ -328,9 +252,9 @@ class _TranslationCache: def _validate_placeholders( self, language: str, - updated_resources: dict[str, Any], - cached_resources: dict[str, Any] | None = None, - ) -> dict[str, Any]: + updated_resources: dict[str, str], + cached_resources: dict[str, str] | None = None, + ) -> dict[str, str]: """Validate if updated resources have same placeholders as cached resources.""" if cached_resources is None: return updated_resources @@ -355,10 +279,12 @@ class _TranslationCache: _LOGGER.error( ( "Validation of translation placeholders for localized (%s) string " - "%s failed" + "%s failed: (%s != %s)" ), language, key, + updated_placeholders, + cached_placeholders, ) mismatches.add(key) @@ -377,38 +303,27 @@ class _TranslationCache: """Extract resources into the cache.""" resource: dict[str, Any] | str cached = self.cache.setdefault(language, {}) - categories: set[str] = set() - for resource in translation_strings.values(): - categories.update(resource) + categories = { + category + for component in translation_strings.values() + for category in component + } for category in categories: - new_resources: Mapping[str, dict[str, Any] | str] - - if category in ("state", "entity_component"): - new_resources = _merge_resources( - translation_strings, components, category - ) - else: - new_resources = build_resources( - translation_strings, components, category - ) - + new_resources = build_resources(translation_strings, components, category) category_cache = cached.setdefault(category, {}) for component, resource in new_resources.items(): component_cache = category_cache.setdefault(component, {}) - if isinstance(resource, dict): - resources_flatten = recursive_flatten( - f"component.{component}.{category}.", - resource, - ) - resources_flatten = self._validate_placeholders( - language, resources_flatten, component_cache - ) - component_cache.update(resources_flatten) - else: + if not isinstance(resource, dict): component_cache[f"component.{component}.{category}"] = resource + continue + + prefix = f"component.{component}.{category}." + flat = recursive_flatten(prefix, resource) + flat = self._validate_placeholders(language, flat, component_cache) + component_cache.update(flat) @bind_hass @@ -430,7 +345,7 @@ async def async_get_translations( elif integrations is not None: components = set(integrations) else: - components = _async_get_components(hass, category) + components = hass.config.top_level_components return await _async_get_translations_cache(hass).async_fetch( language, category, components @@ -449,11 +364,7 @@ def async_get_cached_translations( If integration is specified, return translations for it. Otherwise, default to all loaded integrations. """ - if integration is not None: - components = {integration} - else: - components = _async_get_components(hass, category) - + components = {integration} if integration else hass.config.top_level_components return _async_get_translations_cache(hass).get_cached( language, category, components ) @@ -466,21 +377,6 @@ def _async_get_translations_cache(hass: HomeAssistant) -> _TranslationCache: return cache -_DIRECT_MAPPED_CATEGORIES = {"state", "entity_component", "services"} - - -@callback -def _async_get_components( - hass: HomeAssistant, - category: str, -) -> set[str]: - """Return a set of components for which translations should be loaded.""" - if category in _DIRECT_MAPPED_CATEGORIES: - return hass.config.components - # Only 'state' supports merging, so remove platforms from selection - return {component for component in hass.config.components if "." not in component} - - @callback def async_setup(hass: HomeAssistant) -> None: """Create translation cache and register listeners for translation loaders. @@ -590,13 +486,4 @@ def async_translate_state( if localize_key in translations: return translations[localize_key] - translations = async_get_cached_translations(hass, language, "state", domain) - if device_class is not None: - localize_key = f"component.{domain}.state.{device_class}.{state}" - if localize_key in translations: - return translations[localize_key] - localize_key = f"component.{domain}.state._.{state}" - if localize_key in translations: - return translations[localize_key] - return state diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 0f372689809..cf97e92d6be 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -2,19 +2,25 @@ from collections.abc import Mapping from enum import Enum -from typing import Any, TypeVar +from functools import partial +from typing import Any, Never import homeassistant.core -_DataT = TypeVar("_DataT") +from .deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) GPSType = tuple[float, float] ConfigType = dict[str, Any] -ContextType = homeassistant.core.Context DiscoveryInfoType = dict[str, Any] ServiceDataType = dict[str, Any] StateType = str | int | float | None TemplateVarsType = Mapping[str, Any] | None +NoEventData = Mapping[str, Never] # Custom type for recorder Queries QueryType = Any @@ -33,7 +39,23 @@ UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access # are not present in the core code base. # 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 +# Deprecated as of 2024.5 use types from homeassistant.core instead. +_DEPRECATED_ContextType = DeprecatedAlias( + homeassistant.core.Context, "homeassistant.core.Context", "2025.5" +) +_DEPRECATED_EventType = DeprecatedAlias( + homeassistant.core.Event, "homeassistant.core.Event", "2025.5" +) +_DEPRECATED_HomeAssistantType = DeprecatedAlias( + homeassistant.core.HomeAssistant, "homeassistant.core.HomeAssistant", "2025.5" +) +_DEPRECATED_ServiceCallType = DeprecatedAlias( + homeassistant.core.ServiceCall, "homeassistant.core.ServiceCall", "2025.5" +) + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 98e635e5ac7..17a690dfc37 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -136,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, run_immediately=True + EVENT_HOMEASSISTANT_STOP, _on_hass_stop ) @callback @@ -378,14 +378,12 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err - raise err + raise except Exception as err: # pylint: disable=broad-except self.last_exception = err self.last_update_success = False - self.logger.exception( - "Unexpected error fetching %s data: %s", self.name, err - ) + self.logger.exception("Unexpected error fetching %s data", self.name) else: if not self.last_update_success: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 48fd3cd54c2..1a72c8eb351 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -11,6 +11,7 @@ from collections.abc import Callable, Iterable from contextlib import suppress from dataclasses import dataclass import functools as ft +from functools import cached_property import importlib import logging import os @@ -41,15 +42,11 @@ from .generated.zeroconf import HOMEKIT, ZEROCONF from .util.json import JSON_DECODE_EXCEPTIONS, json_loads if TYPE_CHECKING: - from functools import cached_property - # The relative imports below are guarded by TYPE_CHECKING # because they would cause a circular import otherwise. from .config_entries import ConfigEntry from .helpers import device_registry as dr from .helpers.typing import ConfigType -else: - from .backports.functools import cached_property _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) @@ -979,6 +976,8 @@ class Integration: comp = await self.hass.async_add_import_executor_job( self._get_component, True ) + except ModuleNotFoundError: + raise except ImportError as ex: load_executor = False _LOGGER.debug( @@ -1118,6 +1117,8 @@ class Integration: self._load_platforms, platform_names ) ) + except ModuleNotFoundError: + raise except ImportError as ex: _LOGGER.debug( "Failed to import %s platforms %s in executor", @@ -1549,6 +1550,20 @@ class Helpers: def __getattr__(self, helper_name: str) -> ModuleWrapper: """Fetch a helper.""" helper = importlib.import_module(f"homeassistant.helpers.{helper_name}") + + # Local import to avoid circular dependencies + from .helpers.frame import report # pylint: disable=import-outside-toplevel + + report( + ( + f"accesses hass.helpers.{helper_name}." + " This is deprecated and will stop working in Home Assistant 2024.11, it" + f" should be updated to import functions used from {helper_name} directly" + ), + error_if_core=False, + log_custom_component_only=True, + ) + wrapped = ModuleWrapper(self._hass, helper) setattr(self, helper_name, wrapped) return wrapped diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b2f55381f4d..b1c0391022a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,11 +1,13 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodhcpwatcher==1.0.0 -aiodiscover==2.0.0 +aiodiscover==2.1.0 +aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-zlib-ng==0.3.1 +aiohttp-isal==0.2.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 +aiohttp_session==2.12.0 astral==2.2 async-interrupt==1.1.1 async-upnp-client==0.38.3 @@ -15,8 +17,8 @@ awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 -bluetooth-adapters==0.18.0 -bluetooth-auto-recovery==1.4.0 +bluetooth-adapters==0.19.1 +bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 @@ -26,12 +28,12 @@ dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==2.4.2 +habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240404.2 -home-assistant-intents==2024.4.3 +home-assistant-frontend==20240501.0 +home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.3 @@ -49,11 +51,11 @@ pyOpenSSL==24.1.0 pyserial==3.5 python-slugify==8.0.4 PyTurboJPEG==1.7.1 -pyudev==0.23.2 +pyudev==0.24.1 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.29 -typing-extensions>=4.10.0,<5.0 +typing-extensions>=4.11.0,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous-serialize==2.6.0 diff --git a/homeassistant/py.typed b/homeassistant/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index e78398ebf03..e282ced90ac 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -122,6 +122,11 @@ def _install_requirements_if_missing( return installed, failures +def _set_result_unless_done(future: asyncio.Future[None]) -> None: + if not future.done(): + future.set_result(None) + + class RequirementsManager: """Manage requirements.""" @@ -144,16 +149,13 @@ class RequirementsManager: is invalid, RequirementNotFound if there was some type of failure to install requirements. """ - if done is None: done = {domain} else: done.add(domain) - integration = await async_get_integration(self.hass, domain) - if self.hass.config.skip_pip: - return integration + return await async_get_integration(self.hass, domain) cache = self.integrations_with_reqs int_or_fut = cache.get(domain, UNDEFINED) @@ -170,19 +172,19 @@ class RequirementsManager: if int_or_fut is not UNDEFINED: return cast(Integration, int_or_fut) - event = cache[domain] = self.hass.loop.create_future() + future = cache[domain] = self.hass.loop.create_future() try: + integration = await async_get_integration(self.hass, domain) await self._async_process_integration(integration, done) except Exception: del cache[domain] - if not event.done(): - event.set_result(None) raise + finally: + _set_result_unless_done(future) cache[domain] = integration - if not event.done(): - event.set_result(None) + _set_result_unless_done(future) return integration async def _async_process_integration( diff --git a/homeassistant/runner.py b/homeassistant/runner.py index f036c7d6322..4e2326d4ea7 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -107,6 +107,7 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): def new_event_loop(self) -> asyncio.AbstractEventLoop: """Get the event loop.""" loop: asyncio.AbstractEventLoop = super().new_event_loop() + setattr(loop, "_thread_ident", threading.get_ident()) loop.set_exception_handler(_async_loop_exception_handler) if self.debug: loop.set_debug(True) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 2e64fefee77..86df6417169 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -8,6 +8,7 @@ from collections.abc import Awaitable, Callable, Generator, Mapping import contextlib import contextvars from enum import StrEnum +from functools import partial import logging.handlers import time from types import ModuleType @@ -166,7 +167,6 @@ async def async_setup_component( setup_future.set_result(result) if setup_done_future := setup_done_futures.pop(domain, None): setup_done_future.set_result(result) - return result except BaseException as err: futures = [setup_future] if setup_done_future := setup_done_futures.pop(domain, None): @@ -185,6 +185,7 @@ async def async_setup_component( # if there are no concurrent setup attempts await future raise + return result async def _async_process_dependencies( @@ -253,34 +254,39 @@ async def _async_process_dependencies( return failed -async def _async_setup_component( # noqa: C901 +def _log_error_setup_error( + hass: HomeAssistant, + domain: str, + integration: loader.Integration | None, + msg: str, + exc_info: Exception | None = None, +) -> None: + """Log helper.""" + if integration is None: + custom = "" + link = None + else: + custom = "" if integration.is_built_in else "custom integration " + link = integration.documentation + _LOGGER.error("Setup failed for %s'%s': %s", custom, domain, msg, exc_info=exc_info) + async_notify_setup_error(hass, domain, link) + + +async def _async_setup_component( hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component for Home Assistant. This method is a coroutine. """ - integration: loader.Integration | None = None - - def log_error(msg: str, exc_info: Exception | None = None) -> None: - """Log helper.""" - if integration is None: - custom = "" - link = None - else: - custom = "" if integration.is_built_in else "custom integration " - link = integration.documentation - _LOGGER.error( - "Setup failed for %s'%s': %s", custom, domain, msg, exc_info=exc_info - ) - async_notify_setup_error(hass, domain, link) - try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: - log_error("Integration not found.") + _log_error_setup_error(hass, domain, None, "Integration not found.") return False + log_error = partial(_log_error_setup_error, hass, domain, integration) + if integration.disabled: log_error(f"Dependency is disabled - {integration.disabled}") return False @@ -428,10 +434,9 @@ async def _async_setup_component( # noqa: C901 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) + # If the integration has a config_flow, wait for import flows. + # As these are all created with eager tasks, we do not sleep here, + # as the tasks will always be started before we reach this point. await hass.config_entries.flow.async_wait_import_flow_initialized(domain) # Add to components before the entry.async_setup @@ -444,7 +449,7 @@ async def _async_setup_component( # noqa: C901 await asyncio.gather( *( create_eager_task( - entry.async_setup(hass, integration=integration), + entry.async_setup_locked(hass, integration=integration), name=f"config entry setup {entry.title} {entry.domain} {entry.entry_id}", ) for entry in entries @@ -452,10 +457,11 @@ async def _async_setup_component( # noqa: C901 ) # Cleanup - if domain in hass.data[DATA_SETUP]: - hass.data[DATA_SETUP].pop(domain) + hass.data[DATA_SETUP].pop(domain, None) - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) + hass.bus.async_fire_internal( + EVENT_COMPONENT_LOADED, EventComponentLoaded(component=domain) + ) return True @@ -483,14 +489,6 @@ async def async_prepare_setup_platform( log_error("Integration not found") return None - # Process deps and reqs as soon as possible, so that requirements are - # available when we import the platform. - try: - await async_process_deps_reqs(hass, hass_config, integration) - except HomeAssistantError as err: - 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. # @@ -498,6 +496,16 @@ async def async_prepare_setup_platform( # where the top level component is. # if load_top_level_component := integration.domain not in hass.config.components: + # Process deps and reqs as soon as possible, so that requirements are + # available when we import the platform. We only do this if the integration + # is not in hass.config.components yet, as we already processed them in + # async_setup_component if it is. + try: + await async_process_deps_reqs(hass, hass_config, integration) + except HomeAssistantError as err: + log_error(str(err)) + return None + try: component = await integration.async_get_component() except ImportError as exc: @@ -592,7 +600,7 @@ def _async_when_setup( _LOGGER.exception("Error handling when_setup callback for %s", component) if component in hass.config.components: - hass.async_create_task( + hass.async_create_task_internal( when_setup(), f"when setup {component}", eager_start=True ) return @@ -615,14 +623,11 @@ def _async_when_setup( EVENT_COMPONENT_LOADED, _matched_event, event_filter=_async_is_component_filter, - run_immediately=True, ) ) if start_event: listeners.append( - hass.bus.async_listen( - EVENT_HOMEASSISTANT_START, _matched_event, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _matched_event) ) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 8c042242e0b..19c20207e1d 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -5,21 +5,15 @@ from __future__ import annotations from asyncio import AbstractEventLoop, Future, Semaphore, Task, gather, get_running_loop from collections.abc import Awaitable, Callable, Coroutine import concurrent.futures -from contextlib import suppress -import functools import logging import threading -from typing import Any, ParamSpec, TypeVar, TypeVarTuple - -from homeassistant.exceptions import HomeAssistantError +from typing import Any, TypeVar, TypeVarTuple _LOGGER = logging.getLogger(__name__) _SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe" _T = TypeVar("_T") -_R = TypeVar("_R") -_P = ParamSpec("_P") _Ts = TypeVarTuple("_Ts") @@ -30,12 +24,20 @@ def create_eager_task( 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, - ) + if not loop: + try: + loop = get_running_loop() + except RuntimeError: + # If there is no running loop, create_eager_task is being called from + # the wrong thread. + # Late import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from homeassistant.helpers import frame + + frame.report("attempted to create an asyncio task from a thread") + raise + + return Task(coro, loop=loop, name=name, eager_start=True) def cancelling(task: Future[Any]) -> bool: @@ -50,8 +52,7 @@ def run_callback_threadsafe( Return a concurrent.futures.Future to access the result. """ - ident = loop.__dict__.get("_thread_ident") - if ident is not None and ident == threading.get_ident(): + if (ident := loop.__dict__.get("_thread_ident")) and ident == threading.get_ident(): raise RuntimeError("Cannot be called from within the event loop") future: concurrent.futures.Future[_T] = concurrent.futures.Future() @@ -92,105 +93,6 @@ def run_callback_threadsafe( return future -def check_loop( - func: Callable[..., Any], strict: bool = True, advise_msg: str | None = None -) -> None: - """Warn if called inside the event loop. Raise if `strict` is True. - - The default advisory message is 'Use `await hass.async_add_executor_job()' - Set `advise_msg` to an alternate message if the solution differs. - """ - try: - get_running_loop() - in_loop = True - except RuntimeError: - in_loop = False - - 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 - - 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() - except MissingIntegrationFrame: - # Did not source from integration? Hard error. - if found_frame is None: - raise RuntimeError( # noqa: TRY200 - f"Detected blocking call to {func.__name__} inside the event loop. " - f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " - "This is causing stability issues. Please create a bug report at " - f"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - - hass: HomeAssistant | None = None - with suppress(HomeAssistantError): - hass = async_get_hass() - report_issue = async_suggest_report_issue( - hass, - integration_domain=integration_frame.integration, - module=integration_frame.module, - ) - - _LOGGER.warning( - ( - "Detected blocking call to %s inside the event loop by %sintegration '%s' " - "at %s, line %s: %s, please %s" - ), - func.__name__, - "custom " if integration_frame.custom_integration else "", - integration_frame.integration, - integration_frame.relative_filename, - integration_frame.line_number, - integration_frame.line, - report_issue, - ) - - if strict: - 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 {integration_frame.line_number}:" - f" {integration_frame.line}" - ) - - -def protect_loop(func: Callable[_P, _R], strict: bool = True) -> Callable[_P, _R]: - """Protect function from running in event loop.""" - - @functools.wraps(func) - def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: - check_loop(func, strict=strict) - return func(*args, **kwargs) - - return protected_loop_func - - async def gather_with_limited_concurrency( limit: int, *tasks: Any, return_exceptions: bool = False ) -> Any: diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 39976cce5f7..923838a48a5 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -100,7 +100,7 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: # We use a partial here since it is implemented in native code # and avoids the global lookup of UTC -utcnow: partial[dt.datetime] = partial(dt.datetime.now, UTC) +utcnow = partial(dt.datetime.now, UTC) utcnow.__doc__ = "Get now in UTC time." @@ -188,7 +188,7 @@ 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_str: str, *, raise_on_error: Literal[False] ) -> dt.datetime | None: ... @@ -286,36 +286,78 @@ def parse_time(time_str: str) -> dt.time | None: return None -def get_age(date: dt.datetime) -> str: - """Take a datetime and return its "age" as a string. - - The age can be in second, minute, hour, day, month or year. Only the - biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will - be returned. - Make sure date is not in the future, or else it won't work. - """ +def _get_timestring(timediff: float, precision: int = 1) -> str: + """Return a string representation of a time diff.""" def formatn(number: int, unit: str) -> str: """Add "unit" if it's plural.""" if number == 1: - return f"1 {unit}" - return f"{number:d} {unit}s" + return f"1 {unit} " + return f"{number:d} {unit}s " + + if timediff == 0.0: + return "0 seconds" + + units = ("year", "month", "day", "hour", "minute", "second") + + factors = (365 * 24 * 60 * 60, 30 * 24 * 60 * 60, 24 * 60 * 60, 60 * 60, 60, 1) + + result_string: str = "" + current_precision = 0 + + for i, current_factor in enumerate(factors): + selected_unit = units[i] + if timediff < current_factor: + continue + current_precision = current_precision + 1 + if current_precision == precision: + return ( + result_string + formatn(round(timediff / current_factor), selected_unit) + ).rstrip() + curr_diff = int(timediff // current_factor) + result_string += formatn(curr_diff, selected_unit) + timediff -= (curr_diff) * current_factor + + return result_string.rstrip() + + +def get_age(date: dt.datetime, precision: int = 1) -> str: + """Take a datetime and return its "age" as a string. + + The age can be in second, minute, hour, day, month and year. + + depth number of units will be returned, with the last unit rounded + + The date must be in the past or a ValueException will be raised. + """ delta = (now() - date).total_seconds() + rounded_delta = round(delta) - units = ["second", "minute", "hour", "day", "month"] - factors = [60, 60, 24, 30, 12] - selected_unit = "year" + if rounded_delta < 0: + raise ValueError("Time value is in the future") + return _get_timestring(rounded_delta, precision) - for i, next_factor in enumerate(factors): - if rounded_delta < next_factor: - selected_unit = units[i] - break - delta /= next_factor - rounded_delta = round(delta) - return formatn(rounded_delta, selected_unit) +def get_time_remaining(date: dt.datetime, precision: int = 1) -> str: + """Take a datetime and return its "age" as a string. + + The age can be in second, minute, hour, day, month and year. + + depth number of units will be returned, with the last unit rounded + + The date must be in the future or a ValueException will be raised. + """ + + delta = (date - now()).total_seconds() + + rounded_delta = round(delta) + + if rounded_delta < 0: + raise ValueError("Time value is in the past") + + return _get_timestring(rounded_delta, precision) def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]: diff --git a/homeassistant/util/event_type.py b/homeassistant/util/event_type.py new file mode 100644 index 00000000000..509a35d33ae --- /dev/null +++ b/homeassistant/util/event_type.py @@ -0,0 +1,22 @@ +"""Implementation for EventType. + +Custom for type checking. See stub file. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Generic + +from typing_extensions import TypeVar + +_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) + + +class EventType(str, Generic[_DataT]): + """Custom type for Event.event_type. + + At runtime this is a generic subclass of str. + """ + + __slots__ = () diff --git a/homeassistant/util/event_type.pyi b/homeassistant/util/event_type.pyi new file mode 100644 index 00000000000..4285e54e8c9 --- /dev/null +++ b/homeassistant/util/event_type.pyi @@ -0,0 +1,25 @@ +"""Stub file for event_type. Provide overload for type checking.""" +# ruff: noqa: PYI021 # Allow docstrings + +from collections.abc import Mapping +from typing import Any, Generic + +from typing_extensions import TypeVar + +__all__ = [ + "EventType", +] + +_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) + +class EventType(Generic[_DataT]): + """Custom type for Event.event_type. At runtime delegated to str. + + For type checkers pretend to be its own separate class. + """ + + def __init__(self, value: str, /) -> None: ... + def __len__(self) -> int: ... + def __hash__(self) -> int: ... + def __eq__(self, value: object, /) -> bool: ... + def __getitem__(self, index: int) -> str: ... diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 489b6493ef1..ab163578846 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable, Coroutine from functools import partial, wraps import inspect @@ -12,7 +11,12 @@ import queue import traceback from typing import Any, TypeVar, TypeVarTuple, cast, overload -from homeassistant.core import HomeAssistant, callback, is_callback +from homeassistant.core import ( + HassJobType, + HomeAssistant, + callback, + get_hassjob_callable_job_type, +) _T = TypeVar("_T") _Ts = TypeVarTuple("_Ts") @@ -23,15 +27,6 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): listener: logging.handlers.QueueListener | None = None - def prepare(self, record: logging.LogRecord) -> logging.LogRecord: - """Prepare a record for queuing. - - This is added as a workaround for https://bugs.python.org/issue46755 - """ - record = super().prepare(record) - record.stack_info = None - return record - def handle(self, record: logging.LogRecord) -> Any: """Conditionally emit the specified logging record. @@ -138,34 +133,38 @@ def _callback_wrapper( @overload def catch_log_exception( - func: Callable[[*_Ts], Coroutine[Any, Any, Any]], format_err: Callable[[*_Ts], Any] + func: Callable[[*_Ts], Coroutine[Any, Any, Any]], + format_err: Callable[[*_Ts], Any], + job_type: HassJobType | None = None, ) -> Callable[[*_Ts], Coroutine[Any, Any, None]]: ... @overload def catch_log_exception( - func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any] + func: Callable[[*_Ts], Any], + format_err: Callable[[*_Ts], Any], + job_type: HassJobType | None = None, ) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: ... def catch_log_exception( - func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any] + func: Callable[[*_Ts], Any], + format_err: Callable[[*_Ts], Any], + job_type: HassJobType | None = None, ) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: """Decorate a function func to catch and log exceptions. If func is a coroutine function, a coroutine function will be returned. If func is a callback, a callback will be returned. """ - # Check for partials to properly determine if coroutine function - check_func = func - while isinstance(check_func, partial): - check_func = check_func.func # type: ignore[unreachable] # false positive + if job_type is None: + job_type = get_hassjob_callable_job_type(func) - if asyncio.iscoroutinefunction(check_func): + if job_type is HassJobType.Coroutinefunction: async_func = cast(Callable[[*_Ts], Coroutine[Any, Any, None]], func) return wraps(async_func)(partial(_async_wrapper, async_func, format_err)) # type: ignore[return-value] - if is_callback(check_func): + if job_type is HassJobType.Callback: return wraps(func)(partial(_callback_wrapper, func, format_err)) # type: ignore[return-value] return wraps(func)(partial(_sync_wrapper, func, format_err)) # type: ignore[return-value] @@ -198,12 +197,10 @@ def async_create_catching_coro( target: target coroutine. """ trace = traceback.extract_stack() - wrapped_target = catch_log_coro_exception( + return catch_log_coro_exception( target, lambda: "Exception in {} called from\n {}".format( target.__name__, "".join(traceback.format_list(trace[:-1])), ), ) - - return wrapped_target diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py new file mode 100644 index 00000000000..f8fe5c701f3 --- /dev/null +++ b/homeassistant/util/loop.py @@ -0,0 +1,146 @@ +"""asyncio loop utilities.""" + +from __future__ import annotations + +from asyncio import get_running_loop +from collections.abc import Callable +from contextlib import suppress +import functools +import linecache +import logging +from typing import Any, ParamSpec, TypeVar + +from homeassistant.core import HomeAssistant, async_get_hass +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.frame import ( + MissingIntegrationFrame, + get_current_frame, + get_integration_frame, +) +from homeassistant.loader import async_suggest_report_issue + +_LOGGER = logging.getLogger(__name__) + + +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def _get_line_from_cache(filename: str, lineno: int) -> str: + """Get line from cache or read from file.""" + return (linecache.getline(filename, lineno) or "?").strip() + + +def check_loop( + func: Callable[..., Any], + check_allowed: Callable[[dict[str, Any]], bool] | None = None, + strict: bool = True, + strict_core: bool = True, + advise_msg: str | None = None, + **mapped_args: Any, +) -> None: + """Warn if called inside the event loop. Raise if `strict` is True. + + The default advisory message is 'Use `await hass.async_add_executor_job()' + Set `advise_msg` to an alternate message if the solution differs. + """ + try: + get_running_loop() + in_loop = True + except RuntimeError: + in_loop = False + + if not in_loop: + return + + if check_allowed is not None and check_allowed(mapped_args): + return + + found_frame = None + offender_frame = get_current_frame(2) + offender_filename = offender_frame.f_code.co_filename + offender_lineno = offender_frame.f_lineno + offender_line = _get_line_from_cache(offender_filename, offender_lineno) + + try: + integration_frame = get_integration_frame() + except MissingIntegrationFrame: + # Did not source from integration? Hard error. + if not strict_core: + _LOGGER.warning( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + ) + return + + if found_frame is None: + raise RuntimeError( # noqa: TRY200 + f"Detected blocking call to {func.__name__} inside the event loop " + f"in {offender_filename}, line {offender_lineno}: {offender_line}. " + f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " + "This is causing stability issues. Please create a bug report at " + f"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + + hass: HomeAssistant | None = None + with suppress(HomeAssistantError): + hass = async_get_hass() + report_issue = async_suggest_report_issue( + hass, + integration_domain=integration_frame.integration, + module=integration_frame.module, + ) + + _LOGGER.warning( + ( + "Detected blocking call to %s inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s" + ), + func.__name__, + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + ) + + if strict: + 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 {integration_frame.line_number}:" + f" {integration_frame.line} " + f"(offender: {offender_filename}, line {offender_lineno}: {offender_line})" + ) + + +def protect_loop( + func: Callable[_P, _R], + strict: bool = True, + strict_core: bool = True, + check_allowed: Callable[[dict[str, Any]], bool] | None = None, +) -> Callable[_P, _R]: + """Protect function from running in event loop.""" + + @functools.wraps(func) + def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: + check_loop( + func, + strict=strict, + strict_core=strict_core, + check_allowed=check_allowed, + args=args, + kwargs=kwargs, + ) + return func(*args, **kwargs) + + return protected_loop_func diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 44f9be3272f..067bf5ff36d 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -147,5 +147,4 @@ async def async_get_user_site(deps_dir: str) -> str: close_fds=False, # required for posix_spawn ) stdout, _ = await process.communicate() - lib_dir = stdout.decode().strip() - return lib_dir + return stdout.decode().strip() diff --git a/homeassistant/util/signal_type.py b/homeassistant/util/signal_type.py index be634ce6ba9..e2730c969c4 100644 --- a/homeassistant/util/signal_type.py +++ b/homeassistant/util/signal_type.py @@ -19,7 +19,7 @@ class _SignalTypeBase(Generic[*_Ts]): return hash(self.name) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Check equality for dict keys to be compatible with str.""" if isinstance(other, str): diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index cf73ee6b220..72cabffeed6 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -39,9 +39,9 @@ class _GlobalFreezeContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._exit() return None @@ -52,9 +52,9 @@ class _GlobalFreezeContext: def __exit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._loop.call_soon_threadsafe(self._exit) return None @@ -107,9 +107,9 @@ class _ZoneFreezeContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._exit() return None @@ -120,9 +120,9 @@ class _ZoneFreezeContext: def __exit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._loop.call_soon_threadsafe(self._exit) return None @@ -171,9 +171,9 @@ class _GlobalTaskContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._stop_timer() self._manager.global_tasks.remove(self) @@ -286,9 +286,9 @@ class _ZoneTaskContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._zone.exit_task(self) self._stop_timer() @@ -472,8 +472,7 @@ class TimeoutManager: # Global Zone if zone_name == ZONE_GLOBAL: - task = _GlobalTaskContext(self, current_task, timeout, cool_down) - return task + return _GlobalTaskContext(self, current_task, timeout, cool_down) # Zone Handling if zone_name in self.zones: diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 28027c97211..0809e86460b 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -8,7 +8,7 @@ from io import StringIO, TextIOWrapper import logging import os from pathlib import Path -from typing import TYPE_CHECKING, Any, TextIO, TypeVar, overload +from typing import Any, TextIO, overload import yaml @@ -22,22 +22,17 @@ except ImportError: SafeLoader as FastestAvailableSafeLoader, ) +from functools import cached_property + from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.frame import report from .const import SECRET_YAML from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - # mypy: allow-untyped-calls, no-warn-return-any JSON_TYPE = list | dict | str -_DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) @@ -280,7 +275,7 @@ def _parse_yaml_python( def _parse_yaml( - loader: type[FastSafeLoader] | type[PythonSafeLoader], + loader: type[FastSafeLoader | PythonSafeLoader], content: str | TextIO, secrets: Secrets | None = None, ) -> JSON_TYPE: @@ -290,37 +285,37 @@ def _parse_yaml( @overload def _add_reference( - obj: list | NodeListClass, - loader: LoaderType, - node: yaml.nodes.Node, + obj: list | NodeListClass, loader: LoaderType, node: yaml.nodes.Node ) -> NodeListClass: ... @overload def _add_reference( - obj: str | NodeStrClass, - loader: LoaderType, - node: yaml.nodes.Node, + obj: str | NodeStrClass, loader: LoaderType, node: yaml.nodes.Node ) -> NodeStrClass: ... @overload def _add_reference( - obj: _DictT, loader: LoaderType, node: yaml.nodes.Node -) -> _DictT: ... + obj: dict | NodeDictClass, loader: LoaderType, node: yaml.nodes.Node +) -> NodeDictClass: ... -def _add_reference( # type: ignore[no-untyped-def] - obj, loader: LoaderType, node: yaml.nodes.Node -): +def _add_reference( + obj: dict | list | str | NodeDictClass | NodeListClass | NodeStrClass, + loader: LoaderType, + node: yaml.nodes.Node, +) -> NodeDictClass | NodeListClass | NodeStrClass: """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) - if isinstance(obj, str): + elif isinstance(obj, str): obj = NodeStrClass(obj) + elif isinstance(obj, dict): + obj = NodeDictClass(obj) try: # suppress is much slower - setattr(obj, "__config_file__", loader.get_name) - setattr(obj, "__line__", node.start_mark.line + 1) + obj.__config_file__ = loader.get_name + obj.__line__ = node.start_mark.line + 1 except AttributeError: pass return obj @@ -428,7 +423,7 @@ def _handle_mapping_tag( nodes = loader.construct_pairs(node) seen: dict = {} - for (key, _), (child_node, _) in zip(nodes, node.value): + for (key, _), (child_node, _) in zip(nodes, node.value, strict=False): line = child_node.start_mark.line try: diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index 70c229c1a2f..d35ba11d25e 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -13,10 +13,20 @@ import yaml class NodeListClass(list): """Wrapper class to be able to add attributes on a list.""" + __slots__ = ("__config_file__", "__line__") + + __config_file__: str + __line__: int | str + class NodeStrClass(str): """Wrapper class to be able to add attributes on a string.""" + __slots__ = ("__config_file__", "__line__") + + __config_file__: str + __line__: int | str + def __voluptuous_compile__(self, schema: vol.Schema) -> Any: """Needed because vol.Schema.compile does not handle str subclasses.""" return _compile_scalar(self) @@ -25,6 +35,11 @@ class NodeStrClass(str): class NodeDictClass(dict): """Wrapper class to be able to add attributes on a dict.""" + __slots__ = ("__config_file__", "__line__") + + __config_file__: str + __line__: int | str + @dataclass(slots=True, frozen=True) class Input: diff --git a/mypy.ini b/mypy.ini index 81f6f553eb6..216d43322a4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4,9 +4,10 @@ [mypy] python_version = 3.12 +platform = linux plugins = pydantic.mypy show_error_codes = true -follow_imports = silent +follow_imports = normal local_partial_types = true strict_equality = true no_implicit_optional = true @@ -420,6 +421,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ambient_network.*] +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.ambient_station.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1421,6 +1432,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.energenie_power_sockets.*] +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.energy.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1461,6 +1482,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.eq3btsmart.*] +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.esphome.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -3371,6 +3402,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ring.*] +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.rituals_perfume_genie.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index b8ec65e4460..d8f85df011f 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -25,6 +25,15 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^StrEnum$"), ), ], + "homeassistant.backports.functools": [ + ObsoleteImportMatch( + reason=( + "We can now use the Python 3.12 provided " + "functools.cached_property instead" + ), + constant=re.compile(r"^cached_property$"), + ), + ], "homeassistant.components.alarm_control_panel": [ ObsoleteImportMatch( reason="replaced by AlarmControlPanelEntityFeature enum", diff --git a/pylint/ruff.toml b/pylint/ruff.toml index ebf53daa903..4e1a0388f31 100644 --- a/pylint/ruff.toml +++ b/pylint/ruff.toml @@ -1,6 +1,11 @@ # This extend our general Ruff rules specifically for tests extend = "../pyproject.toml" +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] + [lint.isort] known-third-party = [ "pylint", diff --git a/pyproject.toml b/pyproject.toml index b6206f107f7..4dd5653f8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools==68.0.0", "wheel~=0.40.0"] +requires = ["setuptools==69.2.0", "wheel~=0.43.0"] build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.4" +version = "2024.5.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -23,10 +23,12 @@ classifiers = [ ] requires-python = ">=3.12.0" dependencies = [ + "aiodns==3.2.0", "aiohttp==3.9.5", "aiohttp_cors==0.7.0", + "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-zlib-ng==0.3.1", + "aiohttp-isal==0.2.0", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", @@ -59,7 +61,7 @@ dependencies = [ "PyYAML==6.0.1", "requests==2.31.0", "SQLAlchemy==2.0.29", - "typing-extensions>=4.10.0,<5.0", + "typing-extensions>=4.11.0,<5.0", "ulid-transform==0.9.0", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 @@ -249,7 +251,7 @@ disable = [ "nested-min-max", # PLW3301 "pointless-statement", # B018 "raise-missing-from", # B904 - # "redefined-builtin", # A001, ruff is way more stricter, needs work + "redefined-builtin", # A001 "try-except-raise", # TRY302 "unused-argument", # ARG001, we don't use it "unused-format-string-argument", #F507 @@ -304,6 +306,10 @@ disable = [ "use-list-literal", # C405 "useless-object-inheritance", # UP004 "useless-return", # PLR1711 + "no-else-break", # RET508 + "no-else-continue", # RET507 + "no-else-raise", # RET506 + "no-else-return", # RET505 # "no-self-use", # PLR6301 # Optional plugin, not enabled # Handled by mypy @@ -479,18 +485,12 @@ filterwarnings = [ # -- fixed, waiting for release / update # 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/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/DataDog/datadogpy/pull/566/files - >=0.37.0 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:datadog.util.compat", # 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 @@ -499,8 +499,6 @@ filterwarnings = [ "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", @@ -509,15 +507,23 @@ filterwarnings = [ "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", + # https://github.com/googleapis/python-pubsub/commit/060f00bcea5cd129be3a2d37078535cc97b4f5e8 - >=2.13.12 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:google.pubsub_v1.services.publisher.client", # 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/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", + # https://github.com/timmo001/system-bridge-connector/pull/27 - >= 4.1.0 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:systembridgeconnector.version", + # https://github.com/jschlyter/ttls/commit/d64f1251397b8238cf6a35bea64784de25e3386c - >=1.8.1 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:ttls", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils", + # -- fixed for Python 3.13 + # https://github.com/rhasspy/wyoming/commit/e34af30d455b6f2bb9e5cfb25fad8d276914bc54 - >=1.4.2 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:wyoming.audio", + # -- other # Locale changes might take some time to resolve upstream "ignore:'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15:DeprecationWarning:micloud.micloud", @@ -544,15 +550,47 @@ filterwarnings = [ # 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 + # https://github.com/pkkid/python-plexapi/pull/1244 - v4.15.11 -> new issue same file + # https://github.com/pkkid/python-plexapi/pull/1370 -> Not fixed here "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", + # - pkg_resources + # https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:aiomusiccast", + # https://github.com/eavanvalkenburg/pysiaalarm/blob/v3.1.1/src/pysiaalarm/data/data.py#L7 - v3.1.1 - 2023-04-17 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pysiaalarm.data.data", + # https://pypi.org/project/pybotvac/ - v0.0.25 - 2024-04-11 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pybotvac.version", + # https://github.com/home-assistant-ecosystem/python-mystrom/blob/2.2.0/pymystrom/__init__.py#L10 - v2.2.0 - 2023-05-21 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pymystrom", + # https://pypi.org/project/velbus-aio/ - v2024.4.0 + # https://github.com/Cereal2nd/velbus-aio/blob/2024.4.0/velbusaio/handler.py#L13 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler", + + # -- Python 3.13 + # HomeAssistant + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:homeassistant.components.assist_pipeline.websocket_api", + # https://pypi.org/project/pylutron/ - v0.2.12 - 2024-02-12 + # https://github.com/thecynic/pylutron/issues/89 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pylutron", + # https://pypi.org/project/SpeechRecognition/ - v3.10.3 - 2024-03-30 + # https://github.com/Uberi/speech_recognition/blob/3.10.3/speech_recognition/__init__.py#L7 + "ignore:'aifc' is deprecated and slated for removal in Python 3.13:DeprecationWarning:speech_recognition", + # https://pypi.org/project/voip-utils/ - v0.1.0 - 2023-06-28 + # https://github.com/home-assistant-libs/voip-utils/blob/v0.1.0/voip_utils/rtp_audio.py#L2 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:voip_utils.rtp_audio", + + # -- Python 3.13 - unmaintained projects, last release about 2+ years + # https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pydub.utils", + # https://github.com/heathbar/plum-lightpad-python/issues/7 - v0.0.11 - 2018-10-16 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:plumlightpad.lightpad", + # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 + # https://github.com/ssaenger/pyws66i/blob/v1.1/pyws66i/__init__.py#L2 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pyws66i", # -- unmaintained projects, last release about 2+ years # https://pypi.org/project/agent-py/ - v0.0.23 - 2020-06-04 @@ -566,6 +604,10 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models", # https://pypi.org/project/foobot_async/ - v1.0.0 - 2020-11-24 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/habitipy/ - v0.3.0 - 2019-01-14 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:habitipy.api", + # https://pypi.org/project/httpsig/ - v1.3.0 - 2018-11-28 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:httpsig", # https://pypi.org/project/influxdb/ - v5.3.1 - 2020-11-11 (archived) "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb.line_protocol", # https://pypi.org/project/lark-parser/ - v0.12.0 - 2021-08-30 -> moved to `lark` @@ -582,6 +624,8 @@ filterwarnings = [ "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/pilight/ - v0.1.1 - 2016-10-19 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pilight", # 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)", @@ -593,6 +637,10 @@ filterwarnings = [ "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/pyowm/ - v3.3.0 - 2022-02-14 + # https://github.com/csparpa/pyowm/issues/435 + # https://github.com/csparpa/pyowm/blob/3.3.0/pyowm/commons/cityidregistry.py#L7 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pyowm.commons.cityidregistry", # 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 @@ -610,18 +658,24 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:webrtcvad", ] +[tool.ruff] +required-version = ">=0.4.1" + [tool.ruff.lint] select = [ + "A001", # Variable {name} is shadowing a Python builtin "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. + "B017", # pytest.raises(BaseException) should be considered evil "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 + "B905", # zip() without an explicit strict= parameter "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings @@ -629,8 +683,10 @@ select = [ "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake + "FLY", # flynt "G", # flake8-logging-format "I", # isort + "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "ICN001", # import concentions; {name} should be imported as {asname} "LOG", # flake8-logging @@ -638,13 +694,18 @@ select = [ "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 + "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RET", # flake8-return "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF013", # PEP 484 prohibits implicit Optional + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions @@ -665,11 +726,11 @@ select = [ "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection "SIM", # flake8-simplify + "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print "TID251", # Banned imports - "TRY004", # Prefer TypeError exception for invalid type - "TRY302", # Remove exception handler; error is immediately re-raised + "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle ] @@ -681,7 +742,6 @@ ignore = [ "D406", # Section name should end with a newline "D407", # Section name underlining "E501", # line too long - "E731", # do not assign a lambda expression, use a def "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}) @@ -694,11 +754,15 @@ ignore = [ "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 + "RUF001", # String contains ambiguous unicode character. + "RUF002", # Docstring contains ambiguous unicode character. + "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice "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 + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` # 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 @@ -720,7 +784,13 @@ ignore = [ "PLE0605", # temporarily disabled - "PT019" + "PT019", + "PYI024", # Use typing.NamedTuple instead of collections.namedtuple + "RET503", + "RET502", + "RET501", + "TRY002", + "TRY301" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] diff --git a/requirements.txt b/requirements.txt index 38bea26a8b6..44c60aec07a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,12 @@ -c homeassistant/package_constraints.txt # Home Assistant Core +aiodns==3.2.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 +aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-zlib-ng==0.3.1 +aiohttp-isal==0.2.0 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 @@ -34,7 +36,7 @@ python-slugify==8.0.4 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.29 -typing-extensions>=4.10.0,<5.0 +typing-extensions>=4.11.0,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 2228c9d1bd6..f391511e607 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,7 +140,7 @@ TwitterAPI==2.7.12 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.1.1 +accuweather==3.0.0 # homeassistant.components.adax adax==0.4.0 @@ -185,11 +185,12 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.7 +aioairzone-cloud==0.5.1 # homeassistant.components.airzone aioairzone==0.7.6 +# homeassistant.components.ambient_network # homeassistant.components.ambient_station aioambient==2024.01.0 @@ -203,16 +204,16 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.4 +aioautomower==2024.4.3 # homeassistant.components.azure_devops -aioazuredevops==1.3.5 +aioazuredevops==2.0.0 # homeassistant.components.baf aiobafi6==0.9.0 # homeassistant.components.aws -aiobotocore==2.9.1 +aiobotocore==2.12.1 # homeassistant.components.comelit aiocomelit==0.9.0 @@ -221,10 +222,10 @@ aiocomelit==0.9.0 aiodhcpwatcher==1.0.0 # homeassistant.components.dhcp -aiodiscover==2.0.0 +aiodiscover==2.1.0 # homeassistant.components.dnsip -aiodns==3.1.1 +aiodns==3.2.0 # homeassistant.components.eafm aioeafm==0.1.2 @@ -242,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==23.2.0 +aioesphomeapi==24.3.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -251,7 +252,7 @@ aioflo==2021.11.0 aioftp==0.21.3 # homeassistant.components.github -aiogithubapi==22.10.1 +aiogithubapi==23.11.0 # homeassistant.components.guardian aioguardian==2022.07.0 @@ -262,16 +263,6 @@ aioharmony==0.2.10 # homeassistant.components.homekit_controller aiohomekit==3.1.5 -# homeassistant.components.http -aiohttp-fast-url-dispatcher==0.3.0 - -# homeassistant.components.http -aiohttp-zlib-ng==0.3.1 - -# homeassistant.components.emulated_hue -# homeassistant.components.http -aiohttp_cors==0.7.0 - # homeassistant.components.hue aiohue==4.7.1 @@ -288,10 +279,10 @@ aiokef==0.2.16 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.4.10 +aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.0 +aiolifx==1.0.2 # homeassistant.components.livisi aiolivisi==0.0.19 @@ -327,7 +318,7 @@ aioopenexchangerates==0.4.0 aiooui==0.1.5 # homeassistant.components.pegel_online -aiopegelonline==0.0.9 +aiopegelonline==0.0.10 # homeassistant.components.acmeda aiopulse==0.4.4 @@ -368,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.2.0 +aioshelly==9.0.0 # homeassistant.components.skybell aioskybell==22.7.0 @@ -376,6 +367,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==3.0.0 +# homeassistant.components.solaredge +aiosolaredge==0.2.0 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -392,7 +386,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==75 +aiounifi==76 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -461,7 +455,7 @@ apple_weatherkit==1.1.2 apprise==1.7.4 # homeassistant.components.aprs -aprslib==0.7.0 +aprslib==0.7.2 # homeassistant.components.aqualogic aqualogic==2.6 @@ -473,7 +467,7 @@ aranet4==2.3.3 arcam-fmj==1.4.0 # homeassistant.components.arris_tg2492lg -arris-tg2492lg==1.2.1 +arris-tg2492lg==2.2.0 # homeassistant.components.ampio asmog==0.0.6 @@ -489,6 +483,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.38.3 +# homeassistant.components.arve +asyncarve==0.0.9 + # homeassistant.components.keyboard_remote asyncinotify==4.0.2 @@ -544,14 +541,15 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.38.1 +bellows==0.38.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.14.6 +bimmer-connected[china]==0.15.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.eq3btsmart # homeassistant.components.esphome bleak-esphome==1.0.0 @@ -581,10 +579,10 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.18.0 +bluetooth-adapters==0.19.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.0 +bluetooth-auto-recovery==1.4.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -596,17 +594,17 @@ bluetooth-data-tools==1.19.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.82 +boschshcpy==0.2.91 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.33.13 +boto3==1.34.51 # homeassistant.components.bring bring-api==0.5.7 # homeassistant.components.broadlink -broadlink==0.18.3 +broadlink==0.19.0 # homeassistant.components.brother brother==4.1.0 @@ -663,7 +661,7 @@ colorthief==0.2.1 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.8 +connect-box==0.3.1 # homeassistant.components.xiaomi_miio construct==2.10.68 @@ -699,7 +697,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==6.0.2 +deebot-client==7.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -791,7 +789,7 @@ elvia==0.1.0 emoji==2.8.0 # homeassistant.components.emulated_roku -emulated-roku==0.2.1 +emulated-roku==0.3.0 # homeassistant.components.huisbaasje energyflip-client==0.2.2 @@ -811,14 +809,17 @@ env-canada==0.6.0 # homeassistant.components.season ephem==4.1.5 +# homeassistant.components.epic_games_store +epicstore-api==0.1.7 + # homeassistant.components.epion epion==0.0.3 # homeassistant.components.epson epson-projector==0.5.1 -# homeassistant.components.epsonworkforce -epsonprinter==0.0.9 +# homeassistant.components.eq3btsmart +eq3btsmart==1.1.6 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 @@ -899,7 +900,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.5 +fyta_cli==0.4.1 # homeassistant.components.google_translate gTTS==2.2.4 @@ -939,19 +940,19 @@ georss-qld-bushfire-alert-client==0.7 getmac==0.9.4 # homeassistant.components.gios -gios==3.2.2 +gios==4.0.0 # homeassistant.components.gitter gitterpy==0.1.7 # homeassistant.components.glances -glances-api==0.5.0 +glances-api==0.6.0 # homeassistant.components.goalzero goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.2.32 +goodwe==0.3.2 # homeassistant.components.google_mail # homeassistant.components.google_tasks @@ -967,7 +968,7 @@ google-cloud-texttospeech==2.12.3 google-generativeai==0.3.1 # homeassistant.components.nest -google-nest-sdm==3.0.3 +google-nest-sdm==3.0.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -979,7 +980,7 @@ goslide-api==0.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.31.0 +govee-ble==0.31.2 # homeassistant.components.govee_light_local govee-local-api==1.4.4 @@ -1003,7 +1004,7 @@ greenwavereality==0.5.1 gridnet==5.0.0 # homeassistant.components.growatt_server -growattServer==1.3.0 +growattServer==1.5.0 # homeassistant.components.google_sheets gspread==5.5.0 @@ -1025,7 +1026,7 @@ ha-av==10.1.1 ha-ffmpeg==3.2.0 # homeassistant.components.iotawatt -ha-iotawattpy==0.1.1 +ha-iotawattpy==0.1.2 # homeassistant.components.philips_js ha-philipsjs==3.1.1 @@ -1034,7 +1035,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.4.2 +habluetooth==2.8.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 @@ -1074,13 +1075,13 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.46 +holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240404.2 +home-assistant-frontend==20240501.0 # homeassistant.components.conversation -home-assistant-intents==2024.4.3 +home-assistant-intents==2024.4.24 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1148,7 +1149,7 @@ influxdb==5.3.1 inkbird-ble==0.5.6 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.4.0 +insteon-frontend-home-assistant==0.5.0 # homeassistant.components.intellifire intellifire4py==2.2.2 @@ -1334,7 +1335,7 @@ motionblindsble==0.0.9 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.2.1.150.6 +mozart-api==3.4.1.8.5 # homeassistant.components.mullvad mullvad-api==1.0.0 @@ -1367,7 +1368,7 @@ netdata==1.1.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==2.2.2 +nettigo-air-monitor==3.0.0 # homeassistant.components.neurio_energy neurio==0.3.1 @@ -1382,7 +1383,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==2.1.0 +nextdns==3.0.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 @@ -1482,7 +1483,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.3 +opower==0.4.4 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1497,7 +1498,7 @@ orvibo==1.1.2 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==1.2.0 +ovoenergy==2.0.0 # homeassistant.components.p1_monitor p1monitor==3.0.0 @@ -1547,7 +1548,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.1 +plugwise==0.37.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1562,7 +1563,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer-times-calculator==0.0.12 +prayer-times-calculator-offline==1.0.3 # homeassistant.components.proliphix proliphix==0.4.1 @@ -1630,7 +1631,7 @@ py-schluter==0.1.7 py-sucks==0.9.9 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.4 +py-synologydsm-api==2.4.2 # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -1660,7 +1661,7 @@ pyEmby==1.9 pyHik==0.3.2 # homeassistant.components.rfxtrx -pyRFXtrx==0.31.0 +pyRFXtrx==0.31.1 # homeassistant.components.sony_projector pySDCP==1 @@ -1795,6 +1796,9 @@ pyedimax==0.2.1 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.energenie_power_sockets +pyegps==0.2.5 + # homeassistant.components.enphase_envoy pyenphase==1.20.1 @@ -1814,7 +1818,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.6 +pyfibaro==0.7.8 # homeassistant.components.fido pyfido==2.1.2 @@ -1835,7 +1839,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.10 +pyfritzhome==0.6.11 # homeassistant.components.ifttt pyfttt==0.3 @@ -1898,7 +1902,7 @@ pyisy==3.1.14 pyitachip2ir==0.0.7 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.9 +pyjvcprojector==1.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 @@ -1934,7 +1938,7 @@ pylast==5.1.0 pylaunches==1.4.0 # homeassistant.components.lg_netcast -pylgnetcast==0.3.7 +pylgnetcast==0.3.9 # homeassistant.components.forked_daapd pylibrespot-java==0.1.1 @@ -1943,7 +1947,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.11 +pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 @@ -2035,7 +2039,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.9 +pyoverkiz==1.13.10 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -2089,7 +2093,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.6.0 +pyrisco==0.6.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -2190,7 +2194,7 @@ pytfiac==0.4 pythinkingcleaner==0.0.3 # homeassistant.components.motionmount -python-MotionMount==0.3.1 +python-MotionMount==1.0.0 # homeassistant.components.awair python-awair==0.2.4 @@ -2253,7 +2257,7 @@ python-kasa[speedups]==0.6.2.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==5.7.0 +python-matter-server==5.10.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -2287,13 +2291,13 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.40.0 +python-roborock==2.0.0 # homeassistant.components.smarttub python-smarttub==0.0.36 # homeassistant.components.songpal -python-songpal==0.16.1 +python-songpal==0.16.2 # homeassistant.components.tado python-tado==0.17.4 @@ -2336,7 +2340,7 @@ pytrafikverket==0.3.10 pytrydan==0.4.0 # homeassistant.components.usb -pyudev==0.23.2 +pyudev==0.24.1 # homeassistant.components.unifiprotect pyunifiprotect==5.1.2 @@ -2444,7 +2448,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.9 +ring-doorbell[listen]==0.8.11 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2494,6 +2498,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[async,encrypted]==2.6.0 +# homeassistant.components.sanix +sanix==1.0.5 + # homeassistant.components.satel_integra satel-integra==0.3.7 @@ -2556,7 +2563,7 @@ slackclient==2.5.0 slixmpp==1.8.5 # homeassistant.components.smart_meter_texas -smart-meter-texas==0.4.7 +smart-meter-texas==0.5.5 # homeassistant.components.smhi smhi-pkg==1.0.16 @@ -2565,16 +2572,13 @@ smhi-pkg==1.0.16 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.2 +soco==0.30.3 # homeassistant.components.solaredge_local solaredge-local==0.2.3 -# homeassistant.components.solaredge -solaredge==0.0.2 - # homeassistant.components.solax -solax==0.3.2 +solax==3.1.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 @@ -2646,7 +2650,7 @@ surepy==0.9.0 swisshydrodata==0.1.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.0.0 +switchbot-api==2.1.0 # homeassistant.components.synology_srm synology-srm==0.2.0 @@ -2730,7 +2734,7 @@ tololib==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2023.2 +total-connect-client==2023.12.1 # homeassistant.components.tplink_lte tp-connected==0.0.4 @@ -2789,7 +2793,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.1 +vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox vallox-websocket-api==5.1.1 @@ -2865,7 +2869,7 @@ wirelesstagpy==0.8.1 wled==0.17.0 # homeassistant.components.wolflink -wolf-comm==0.0.6 +wolf-comm==0.0.7 # homeassistant.components.wyoming wyoming==1.5.3 @@ -2901,7 +2905,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==2.0.0 +yalexs==3.0.1 # homeassistant.components.yeelight yeelight==0.7.14 @@ -2910,7 +2914,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.2 +yolink-api==0.4.3 # homeassistant.components.youless youless-api==1.0.1 @@ -2934,7 +2938,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.114 +zha-quirks==0.0.115 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2955,13 +2959,13 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.5 +zigpy==0.64.0 # homeassistant.components.zoneminder zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.3 +zwave-js-server-python==0.55.4 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test.txt b/requirements_test.txt index 4dd02246a6e..5470bc2a49d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt astroid==3.1.0 -coverage==7.4.4 +coverage==7.5.0 freezegun==1.4.0 mock-open==1.4.0 mypy-dev==1.10.0a3 @@ -16,21 +16,20 @@ pre-commit==3.7.0 pydantic==1.10.12 pylint==3.1.0 pylint-per-file-ignores==1.3.2 -pipdeptree==2.16.1 +pipdeptree==2.17.0 pytest-asyncio==0.23.6 pytest-aiohttp==1.0.5 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.3.1 pytest-unordered==0.6.0 pytest-picked==0.5.0 -pytest-xdist==3.3.1 +pytest-xdist==3.5.0 pytest==8.1.1 -requests-mock==1.11.0 +requests-mock==1.12.1 respx==0.21.0 syrupy==4.6.1 tqdm==4.66.2 @@ -51,4 +50,4 @@ 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 +uv==0.1.35 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be285822e63..140741518d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ Tami4EdgeAPI==2.1 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.1.1 +accuweather==3.0.0 # homeassistant.components.adax adax==0.4.0 @@ -164,11 +164,12 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.7 +aioairzone-cloud==0.5.1 # homeassistant.components.airzone aioairzone==0.7.6 +# homeassistant.components.ambient_network # homeassistant.components.ambient_station aioambient==2024.01.0 @@ -182,16 +183,16 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.4 +aioautomower==2024.4.3 # homeassistant.components.azure_devops -aioazuredevops==1.3.5 +aioazuredevops==2.0.0 # homeassistant.components.baf aiobafi6==0.9.0 # homeassistant.components.aws -aiobotocore==2.9.1 +aiobotocore==2.12.1 # homeassistant.components.comelit aiocomelit==0.9.0 @@ -200,10 +201,10 @@ aiocomelit==0.9.0 aiodhcpwatcher==1.0.0 # homeassistant.components.dhcp -aiodiscover==2.0.0 +aiodiscover==2.1.0 # homeassistant.components.dnsip -aiodns==3.1.1 +aiodns==3.2.0 # homeassistant.components.eafm aioeafm==0.1.2 @@ -221,13 +222,13 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==23.2.0 +aioesphomeapi==24.3.0 # homeassistant.components.flo aioflo==2021.11.0 # homeassistant.components.github -aiogithubapi==22.10.1 +aiogithubapi==23.11.0 # homeassistant.components.guardian aioguardian==2022.07.0 @@ -238,16 +239,6 @@ aioharmony==0.2.10 # homeassistant.components.homekit_controller aiohomekit==3.1.5 -# homeassistant.components.http -aiohttp-fast-url-dispatcher==0.3.0 - -# homeassistant.components.http -aiohttp-zlib-ng==0.3.1 - -# homeassistant.components.emulated_hue -# homeassistant.components.http -aiohttp_cors==0.7.0 - # homeassistant.components.hue aiohue==4.7.1 @@ -261,10 +252,10 @@ aiokafka==0.10.0 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.4.10 +aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.0 +aiolifx==1.0.2 # homeassistant.components.livisi aiolivisi==0.0.19 @@ -300,7 +291,7 @@ aioopenexchangerates==0.4.0 aiooui==0.1.5 # homeassistant.components.pegel_online -aiopegelonline==0.0.9 +aiopegelonline==0.0.10 # homeassistant.components.acmeda aiopulse==0.4.4 @@ -341,7 +332,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.2.0 +aioshelly==9.0.0 # homeassistant.components.skybell aioskybell==22.7.0 @@ -349,6 +340,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==3.0.0 +# homeassistant.components.solaredge +aiosolaredge==0.2.0 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -365,7 +359,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==75 +aiounifi==76 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -425,7 +419,7 @@ apple_weatherkit==1.1.2 apprise==1.7.4 # homeassistant.components.aprs -aprslib==0.7.0 +aprslib==0.7.2 # homeassistant.components.aranet aranet4==2.3.3 @@ -444,6 +438,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.38.3 +# homeassistant.components.arve +asyncarve==0.0.9 + # homeassistant.components.sleepiq asyncsleepiq==1.5.2 @@ -469,11 +466,12 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.38.1 +bellows==0.38.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.14.6 +bimmer-connected[china]==0.15.2 +# homeassistant.components.eq3btsmart # homeassistant.components.esphome bleak-esphome==1.0.0 @@ -496,10 +494,10 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.18.0 +bluetooth-adapters==0.19.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.0 +bluetooth-auto-recovery==1.4.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -511,13 +509,13 @@ bluetooth-data-tools==1.19.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.82 +boschshcpy==0.2.91 # homeassistant.components.bring bring-api==0.5.7 # homeassistant.components.broadlink -broadlink==0.18.3 +broadlink==0.19.0 # homeassistant.components.brother brother==4.1.0 @@ -577,7 +575,7 @@ dbus-fast==2.21.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==6.0.2 +deebot-client==7.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -648,7 +646,7 @@ elmax-api==0.0.4 elvia==0.1.0 # homeassistant.components.emulated_roku -emulated-roku==0.2.1 +emulated-roku==0.3.0 # homeassistant.components.huisbaasje energyflip-client==0.2.2 @@ -665,12 +663,18 @@ env-canada==0.6.0 # homeassistant.components.season ephem==4.1.5 +# homeassistant.components.epic_games_store +epicstore-api==0.1.7 + # homeassistant.components.epion epion==0.0.3 # homeassistant.components.epson epson-projector==0.5.1 +# homeassistant.components.eq3btsmart +eq3btsmart==1.1.6 + # homeassistant.components.esphome esphome-dashboard-api==1.2.3 @@ -731,7 +735,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.5 +fyta_cli==0.4.1 # homeassistant.components.google_translate gTTS==2.2.4 @@ -768,16 +772,16 @@ georss-qld-bushfire-alert-client==0.7 getmac==0.9.4 # homeassistant.components.gios -gios==3.2.2 +gios==4.0.0 # homeassistant.components.glances -glances-api==0.5.0 +glances-api==0.6.0 # homeassistant.components.goalzero goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.2.32 +goodwe==0.3.2 # homeassistant.components.google_mail # homeassistant.components.google_tasks @@ -790,7 +794,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.3.1 # homeassistant.components.nest -google-nest-sdm==3.0.3 +google-nest-sdm==3.0.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -799,7 +803,7 @@ googlemaps==2.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.31.0 +govee-ble==0.31.2 # homeassistant.components.govee_light_local govee-local-api==1.4.4 @@ -817,7 +821,7 @@ greeneye_monitor==3.0.3 gridnet==5.0.0 # homeassistant.components.growatt_server -growattServer==1.3.0 +growattServer==1.5.0 # homeassistant.components.google_sheets gspread==5.5.0 @@ -836,7 +840,7 @@ ha-av==10.1.1 ha-ffmpeg==3.2.0 # homeassistant.components.iotawatt -ha-iotawattpy==0.1.1 +ha-iotawattpy==0.1.2 # homeassistant.components.philips_js ha-philipsjs==3.1.1 @@ -845,7 +849,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.4.2 +habluetooth==2.8.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 @@ -873,13 +877,13 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.46 +holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240404.2 +home-assistant-frontend==20240501.0 # homeassistant.components.conversation -home-assistant-intents==2024.4.3 +home-assistant-intents==2024.4.24 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -929,7 +933,7 @@ influxdb==5.3.1 inkbird-ble==0.5.6 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.4.0 +insteon-frontend-home-assistant==0.5.0 # homeassistant.components.intellifire intellifire4py==2.2.2 @@ -1073,7 +1077,7 @@ motionblindsble==0.0.9 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.2.1.150.6 +mozart-api==3.4.1.8.5 # homeassistant.components.mullvad mullvad-api==1.0.0 @@ -1100,7 +1104,7 @@ nessclient==1.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==2.2.2 +nettigo-air-monitor==3.0.0 # homeassistant.components.nexia nexia==2.0.8 @@ -1112,7 +1116,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==2.1.0 +nextdns==3.0.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 @@ -1175,8 +1179,11 @@ openerz-api==0.3.0 # homeassistant.components.openhome openhomedevice==2.2.0 +# homeassistant.components.enigma2 +openwebifpy==4.2.4 + # homeassistant.components.opower -opower==0.4.3 +opower==0.4.4 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1185,7 +1192,7 @@ oralb-ble==0.17.6 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==1.2.0 +ovoenergy==2.0.0 # homeassistant.components.p1_monitor p1monitor==3.0.0 @@ -1218,7 +1225,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.1 +plugwise==0.37.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1230,7 +1237,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer-times-calculator==0.0.12 +prayer-times-calculator-offline==1.0.3 # homeassistant.components.prometheus prometheus-client==0.17.1 @@ -1286,7 +1293,7 @@ py-nightscout==1.2.2 py-sucks==0.9.9 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.4 +py-synologydsm-api==2.4.2 # homeassistant.components.seventeentrack py17track==2021.12.2 @@ -1304,7 +1311,7 @@ pyDuotecno==2024.3.2 pyElectra==1.2.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.31.0 +pyRFXtrx==0.31.1 # homeassistant.components.tibber pyTibber==0.28.2 @@ -1394,6 +1401,9 @@ pyeconet==0.1.22 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.energenie_power_sockets +pyegps==0.2.5 + # homeassistant.components.enphase_envoy pyenphase==1.20.1 @@ -1407,7 +1417,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.6 +pyfibaro==0.7.8 # homeassistant.components.fido pyfido==2.1.2 @@ -1425,7 +1435,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.10 +pyfritzhome==0.6.11 # homeassistant.components.ifttt pyfttt==0.3 @@ -1473,7 +1483,7 @@ pyiss==1.0.1 pyisy==3.1.14 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.9 +pyjvcprojector==1.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 @@ -1502,6 +1512,9 @@ pylast==5.1.0 # homeassistant.components.launch_library pylaunches==1.4.0 +# homeassistant.components.lg_netcast +pylgnetcast==0.3.9 + # homeassistant.components.forked_daapd pylibrespot-java==0.1.1 @@ -1509,7 +1522,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.11 +pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 @@ -1583,7 +1596,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.9 +pyoverkiz==1.13.10 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1622,7 +1635,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.6.0 +pyrisco==0.6.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1705,7 +1718,7 @@ pytautulli==23.1.1 pytedee-async==0.2.17 # homeassistant.components.motionmount -python-MotionMount==0.3.1 +python-MotionMount==1.0.0 # homeassistant.components.awair python-awair==0.2.4 @@ -1719,6 +1732,9 @@ python-ecobee-api==0.2.17 # homeassistant.components.fully_kiosk python-fullykiosk==0.0.12 +# homeassistant.components.sms +# python-gammu==3.2.4 + # homeassistant.components.analytics_insights python-homeassistant-analytics==0.6.0 @@ -1735,7 +1751,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter -python-matter-server==5.7.0 +python-matter-server==5.10.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -1763,13 +1779,13 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==0.40.0 +python-roborock==2.0.0 # homeassistant.components.smarttub python-smarttub==0.0.36 # homeassistant.components.songpal -python-songpal==0.16.1 +python-songpal==0.16.2 # homeassistant.components.tado python-tado==0.17.4 @@ -1803,7 +1819,7 @@ pytrafikverket==0.3.10 pytrydan==0.4.0 # homeassistant.components.usb -pyudev==0.23.2 +pyudev==0.24.1 # homeassistant.components.unifiprotect pyunifiprotect==5.1.2 @@ -1887,7 +1903,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.9 +ring-doorbell[listen]==0.8.11 # homeassistant.components.roku rokuecp==0.19.2 @@ -1922,6 +1938,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[async,encrypted]==2.6.0 +# homeassistant.components.sanix +sanix==1.0.5 + # homeassistant.components.screenlogic screenlogicpy==0.10.0 @@ -1963,7 +1982,7 @@ simplisafe-python==2024.01.0 slackclient==2.5.0 # homeassistant.components.smart_meter_texas -smart-meter-texas==0.4.7 +smart-meter-texas==0.5.5 # homeassistant.components.smhi smhi-pkg==1.0.16 @@ -1972,13 +1991,10 @@ smhi-pkg==1.0.16 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.2 - -# homeassistant.components.solaredge -solaredge==0.0.2 +soco==0.30.3 # homeassistant.components.solax -solax==0.3.2 +solax==3.1.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 @@ -2044,7 +2060,7 @@ sunweg==2.1.1 surepy==0.9.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.0.0 +switchbot-api==2.1.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 @@ -2095,7 +2111,7 @@ tololib==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2023.2 +total-connect-client==2023.12.1 # homeassistant.components.tplink_omada tplink-omada-client==1.3.12 @@ -2145,7 +2161,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.1 +vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox vallox-websocket-api==5.1.1 @@ -2209,7 +2225,7 @@ wiffi==1.1.2 wled==0.17.0 # homeassistant.components.wolflink -wolf-comm==0.0.6 +wolf-comm==0.0.7 # homeassistant.components.wyoming wyoming==1.5.3 @@ -2242,13 +2258,13 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==2.0.0 +yalexs==3.0.1 # homeassistant.components.yeelight yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.2 +yolink-api==0.4.3 # homeassistant.components.youless youless-api==1.0.1 @@ -2269,7 +2285,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.114 +zha-quirks==0.0.115 # homeassistant.components.zha zigpy-deconz==0.23.1 @@ -2284,10 +2300,10 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.5 +zigpy==0.64.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.3 +zwave-js-server-python==0.55.4 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index cb64db20dcd..4f21f6d4a0c 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.6 -ruff==0.3.4 +ruff==0.4.1 yamllint==1.35.1 diff --git a/script/alexa_locales.py b/script/alexa_locales.py new file mode 100644 index 00000000000..84bdac4133a --- /dev/null +++ b/script/alexa_locales.py @@ -0,0 +1,67 @@ +"""Check if upstream Alexa locales are subset of the core Alexa supported locales.""" + +from pprint import pprint +import re + +from bs4 import BeautifulSoup +import requests + +from homeassistant.components.alexa import capabilities + +SITE = ( + "https://developer.amazon.com/en-GB/docs/alexa/device-apis/list-of-interfaces.html" +) + + +def run_script() -> None: + """Run the script.""" + response = requests.get(SITE, timeout=10) + soup = BeautifulSoup(response.text, "html.parser") + + table = soup.find("table") + table_body = table.find_all("tbody")[-1] + rows = table_body.find_all("tr") + data = [[ele.text.strip() for ele in row.find_all("td") if ele] for row in rows] + upstream_locales_raw = {row[0]: row[3] for row in data} + language_pattern = re.compile(r"^[a-z]{2}-[A-Z]{2}$") + upstream_locales = { + upstream_interface: { + name + for word in upstream_locale.split(" ") + if (name := word.strip(",")) and language_pattern.match(name) is not None + } + for upstream_interface, upstream_locale in upstream_locales_raw.items() + if upstream_interface.count(".") == 1 # Skip sub-interfaces + } + + interfaces_missing = {} + interfaces_nok = {} + interfaces_ok = {} + + for upstream_interface, upstream_locale in upstream_locales.items(): + core_interface_name = upstream_interface.replace(".", "") + core_interface = getattr(capabilities, core_interface_name, None) + + if core_interface is None: + interfaces_missing[upstream_interface] = upstream_locale + continue + + core_locale = core_interface.supported_locales + + if not upstream_locale.issubset(core_locale): + interfaces_nok[core_interface_name] = core_locale + else: + interfaces_ok[core_interface_name] = core_locale + + print("Missing interfaces:") + pprint(list(interfaces_missing)) + print("\n") + print("Interfaces where upstream locales are not subsets of the core locales:") + pprint(list(interfaces_nok)) + print("\n") + print("Interfaces checked ok:") + pprint(list(interfaces_ok)) + + +if __name__ == "__main__": + run_script() diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1423ce92b89..a5db9997d9d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -17,8 +17,10 @@ from typing import Any from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration -COMMENT_REQUIREMENTS = ( - "Adafruit-BBIO", +# Requirements which can't be installed on all systems because they rely on additional +# system packages. Requirements listed in EXCLUDED_REQUIREMENTS_ALL will be commented-out +# in requirements_all.txt and requirements_test_all.txt. +EXCLUDED_REQUIREMENTS_ALL = { "atenpdu", # depends on pysnmp which is not maintained at this time "avea", # depends on bluepy "avion", @@ -37,10 +39,39 @@ COMMENT_REQUIREMENTS = ( "pyuserinput", "tensorflow", "tf-models-official", -) +} -COMMENT_REQUIREMENTS_NORMALIZED = { - commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS +# Requirements excluded by EXCLUDED_REQUIREMENTS_ALL which should be included when +# building integration wheels for all architectures. +INCLUDED_REQUIREMENTS_WHEELS = { + "decora-wifi", + "evdev", + "pycups", + "python-gammu", + "pyuserinput", +} + + +# Requirements to exclude or include when running github actions. +# Requirements listed in "exclude" will be commented-out in +# requirements_all_{action}.txt +# Requirements listed in "include" must be listed in EXCLUDED_REQUIREMENTS_CI, and +# will be included in requirements_all_{action}.txt + +OVERRIDDEN_REQUIREMENTS_ACTIONS = { + "pytest": {"exclude": set(), "include": {"python-gammu"}}, + "wheels_aarch64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, + # Pandas has issues building on armhf, it is expected they + # will drop the platform in the near future (they consider it + # "flimsy" on 386). The following packages depend on pandas, + # so we comment them out. + "wheels_armhf": { + "exclude": {"env-canada", "noaa-coops", "pyezviz", "pykrakenapi"}, + "include": INCLUDED_REQUIREMENTS_WHEELS, + }, + "wheels_armv7": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, + "wheels_amd64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, + "wheels_i386": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, } IGNORE_PIN = ("colorlog>2.1,<3", "urllib3") @@ -255,6 +286,12 @@ def gather_recursive_requirements( return reqs +def _normalize_package_name(package_name: str) -> str: + """Normalize a package name.""" + # pipdeptree needs lowercase and dash instead of underscore or period as separator + return package_name.lower().replace("_", "-").replace(".", "-") + + def normalize_package_name(requirement: str) -> str: """Return a normalized package name from a requirement string.""" # This function is also used in hassfest. @@ -262,17 +299,25 @@ def normalize_package_name(requirement: str) -> str: if not match: return "" - # pipdeptree needs lowercase and dash instead of underscore as separator - package = match.group(1).lower().replace("_", "-") - - return package + # pipdeptree needs lowercase and dash instead of underscore or period as separator + return _normalize_package_name(match.group(1)) def comment_requirement(req: str) -> bool: """Comment out requirement. Some don't install on all systems.""" - return any( - normalize_package_name(req) == ign for ign in COMMENT_REQUIREMENTS_NORMALIZED - ) + return normalize_package_name(req) in EXCLUDED_REQUIREMENTS_ALL + + +def process_action_requirement(req: str, action: str) -> str: + """Process requirement for a specific github action.""" + normalized_package_name = normalize_package_name(req) + if normalized_package_name in OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["exclude"]: + return f"# {req}" + if normalized_package_name in OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["include"]: + return req + if normalized_package_name in EXCLUDED_REQUIREMENTS_ALL: + return f"# {req}" + return req def gather_modules() -> dict[str, list[str]] | None: @@ -358,6 +403,16 @@ def generate_requirements_list(reqs: dict[str, list[str]]) -> str: return "".join(output) +def generate_action_requirements_list(reqs: dict[str, list[str]], action: str) -> str: + """Generate a pip file based on requirements.""" + output = [] + for pkg, requirements in sorted(reqs.items(), key=itemgetter(0)): + output.extend(f"\n# {req}" for req in sorted(requirements)) + processed_pkg = process_action_requirement(pkg, action) + output.append(f"\n{processed_pkg}\n") + return "".join(output) + + def requirements_output() -> str: """Generate output for requirements.""" output = [ @@ -384,6 +439,18 @@ def requirements_all_output(reqs: dict[str, list[str]]) -> str: return "".join(output) +def requirements_all_action_output(reqs: dict[str, list[str]], action: str) -> str: + """Generate output for requirements_all_{action}.""" + output = [ + f"# Home Assistant Core, full dependency set for {action}\n", + GENERATED_MESSAGE, + "-r requirements.txt\n", + ] + output.append(generate_action_requirements_list(reqs, action)) + + return "".join(output) + + def requirements_test_all_output(reqs: dict[str, list[str]]) -> str: """Generate output for test_requirements.""" output = [ @@ -464,7 +531,7 @@ def diff_file(filename: str, content: str) -> list[str]: ) -def main(validate: bool) -> int: +def main(validate: bool, ci: bool) -> int: """Run the script.""" if not os.path.isfile("requirements_all.txt"): print("Run this from HA root dir") @@ -477,17 +544,28 @@ def main(validate: bool) -> int: reqs_file = requirements_output() reqs_all_file = requirements_all_output(data) + reqs_all_action_files = { + action: requirements_all_action_output(data, action) + for action in OVERRIDDEN_REQUIREMENTS_ACTIONS + } reqs_test_all_file = requirements_test_all_output(data) + # Always calling requirements_pre_commit_output is intentional to ensure + # the code is called by the pre-commit hooks. reqs_pre_commit_file = requirements_pre_commit_output() constraints = gather_constraints() - files = ( + files = [ ("requirements.txt", reqs_file), ("requirements_all.txt", reqs_all_file), ("requirements_test_pre_commit.txt", reqs_pre_commit_file), ("requirements_test_all.txt", reqs_test_all_file), ("homeassistant/package_constraints.txt", constraints), - ) + ] + if ci: + files.extend( + (f"requirements_all_{action}.txt", reqs_all_file) + for action, reqs_all_file in reqs_all_action_files.items() + ) if validate: errors = [] @@ -516,4 +594,5 @@ def main(validate: bool) -> int: if __name__ == "__main__": _VAL = sys.argv[-1] == "validate" - sys.exit(main(_VAL)) + _CI = sys.argv[-1] == "ci" + sys.exit(main(_VAL, _CI)) diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 15e34c23416..04150836dd5 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -12,13 +12,30 @@ BASE = """ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Home Assistant Core -setup.cfg @home-assistant/core +.core_files.yaml @home-assistant/core +.git-blame-ignore-revs @home-assistant/core +.gitattributes @home-assistant/core +.gitignore @home-assistant/core +.hadolint.yaml @home-assistant/core +.pre-commit-config.yaml @home-assistant/core +.prettierignore @home-assistant/core +.yamllint @home-assistant/core pyproject.toml @home-assistant/core +requirements_test.txt @home-assistant/core +/.devcontainer/ @home-assistant/core +/.github/ @home-assistant/core +/.vscode/ @home-assistant/core /homeassistant/*.py @home-assistant/core +/homeassistant/auth/ @home-assistant/core +/homeassistant/backports/ @home-assistant/core /homeassistant/helpers/ @home-assistant/core +/homeassistant/scripts/ @home-assistant/core /homeassistant/util/ @home-assistant/core +/pylint/ @home-assistant/core +/script/ @home-assistant/core # Home Assistant Supervisor +.dockerignore @home-assistant/supervisor build.json @home-assistant/supervisor /machine/ @home-assistant/supervisor /rootfs/ @home-assistant/supervisor diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 64951fb0288..686a6697e49 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -20,18 +20,53 @@ DONT_IGNORE = ( "scene.py", ) +CORE_PREFIX = """# Sorted by hassfest. +# +# To sort, run python3 -m script.hassfest -p coverage + +[run] +source = homeassistant +omit = +""" +COMPONENTS_PREFIX = ( + " # omit pieces of code that rely on external devices being present\n" +) +SUFFIX = """[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # TYPE_CHECKING and @overload blocks are never executed during pytest run + if TYPE_CHECKING: + @overload +""" + def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" coverage_path = config.root / ".coveragerc" not_found: list[str] = [] + unsorted: list[str] = [] checking = False + previous_line = "" with coverage_path.open("rt") as fp: for line in fp: line = line.strip() + if line == COMPONENTS_PREFIX.strip(): + previous_line = "" + continue + if not line or line.startswith("#"): continue @@ -55,7 +90,15 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: not_found.append(line) continue - if not line.startswith("homeassistant/components/") or len(path.parts) != 4: + if line < previous_line: + unsorted.append(line) + previous_line = line + + if not line.startswith("homeassistant/components/"): + continue + + # Ignore sub-directories + if len(path.parts) > 4: continue integration_path = path.parent @@ -81,7 +124,49 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: f"{check} must not be ignored by the .coveragerc file", ) + if unsorted: + config.add_error( + "coverage", + "Paths are unsorted in .coveragerc file. " + "Run python3 -m script.hassfest\n - " + f"{'\n - '.join(unsorted)}", + fixable=True, + ) + if not_found: raise RuntimeError( f".coveragerc references files that don't exist: {', '.join(not_found)}." ) + + +def generate(integrations: dict[str, Integration], config: Config) -> None: + """Sort coverage.""" + coverage_path = config.root / ".coveragerc" + core = [] + components = [] + section = "header" + + with coverage_path.open("rt") as fp: + for line in fp: + if line == "[report]\n": + break + + if section != "core" and line == "omit =\n": + section = "core" + elif section != "components" and line == COMPONENTS_PREFIX: + section = "components" + elif section == "core" and line != "\n": + core.append(line) + elif section == "components" and line != "\n": + components.append(line) + + assert core, "core should be a non-empty list" + assert components, "components should be a non-empty list" + content = ( + f"{CORE_PREFIX}{"".join(sorted(core))}\n" + f"{COMPONENTS_PREFIX}{"".join(sorted(components))}\n" + f"\n{SUFFIX}" + ) + + with coverage_path.open("w") as fp: + fp.write(content) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index d9ec114e5bb..66796d4dd0d 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -32,7 +32,11 @@ class ImportCollector(ast.NodeVisitor): self._cur_fil_dir = fil.relative_to(self.integration.path) self.referenced[self._cur_fil_dir] = set() - self.visit(ast.parse(fil.read_text())) + try: + self.visit(ast.parse(fil.read_text())) + except SyntaxError as e: + e.add_note(f"File: {fil}") + raise self._cur_fil_dir = None def _add_reference(self, reference_domain: str) -> None: @@ -148,14 +152,18 @@ IGNORE_VIOLATIONS = { ("demo", "manual"), # This would be a circular dep ("http", "network"), + ("http", "cloud"), # This would be a circular dep ("zha", "homeassistant_hardware"), ("zha", "homeassistant_sky_connect"), ("zha", "homeassistant_yellow"), + ("homeassistant_sky_connect", "zha"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), "logbook", + # Temporary needed for migration until 2024.10 + ("conversation", "assist_pipeline"), } diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 4e348d4ae6c..e38a238be7d 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -38,14 +38,10 @@ RUN \ 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 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" \ uv pip install \ --no-build \ -r homeassistant/requirements_all.txt; \ diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c02ebd8de2e..fab3d5fcd7f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -32,9 +32,10 @@ HEADER: Final = """ GENERAL_SETTINGS: Final[dict[str, str]] = { "python_version": ".".join(str(x) for x in REQUIRED_PYTHON_VER[:2]), - "plugins": ", ".join(["pydantic.mypy"]), + "platform": "linux", + "plugins": "pydantic.mypy", "show_error_codes": "true", - "follow_imports": "silent", + "follow_imports": "normal", # Enable some checks globally. "local_partial_types": "true", "strict_equality": "true", @@ -43,14 +44,14 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", - "enable_error_code": ", ".join( + "enable_error_code": ", ".join( # noqa: FLY002 [ "ignore-without-code", "redundant-self", "truthy-iterable", ] ), - "disable_error_code": ", ".join( + "disable_error_code": ", ".join( # noqa: FLY002 [ "annotation-unchecked", "import-not-found", diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 18d560f840f..2c4ed47b158 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -15,28 +15,22 @@ from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from tqdm import tqdm import homeassistant.util.package as pkg_util -from script.gen_requirements_all import COMMENT_REQUIREMENTS, normalize_package_name +from script.gen_requirements_all import ( + EXCLUDED_REQUIREMENTS_ALL, + normalize_package_name, +) from .model import Config, Integration -IGNORE_PACKAGES = { - commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS -} PACKAGE_REGEX = re.compile( r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" ) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") -IGNORE_VIOLATIONS = { - # Still has standard library requirements. - "acmeda", - "blink", - "ezviz", - "hdmi_cec", - "juicenet", - "lupusec", - "rainbird", +IGNORE_STANDARD_LIBRARY_VIOLATIONS = { + # Integrations which have standard library requirements. + "electrasmart", "slide", "suez_water", } @@ -112,10 +106,6 @@ def validate_requirements(integration: Integration) -> None: if not validate_requirements_format(integration): return - # Some integrations have not been fixed yet so are allowed to have violations. - if integration.domain in IGNORE_VIOLATIONS: - return - integration_requirements = set() integration_packages = set() for req in integration.requirements: @@ -126,7 +116,7 @@ def validate_requirements(integration: Integration) -> None: f"Failed to normalize package name from requirement {req}", ) return - if (package == ign for ign in IGNORE_PACKAGES): + if package in EXCLUDED_REQUIREMENTS_ALL: continue integration_requirements.add(req) integration_packages.add(package) @@ -149,12 +139,34 @@ def validate_requirements(integration: Integration) -> None: return # Check for requirements incompatible with standard library. + standard_library_violations = set() for req in all_integration_requirements: - if req in sys.stlib_module_names: - integration.add_error( - "requirements", - f"Package {req} is not compatible with the Python standard library", - ) + if req in sys.stdlib_module_names: + standard_library_violations.add(req) + + if ( + standard_library_violations + and integration.domain not in IGNORE_STANDARD_LIBRARY_VIOLATIONS + ): + integration.add_error( + "requirements", + ( + f"Package {req} has dependencies {standard_library_violations} which " + "are not compatible with the Python standard library" + ), + ) + elif ( + not standard_library_violations + and integration.domain in IGNORE_STANDARD_LIBRARY_VIOLATIONS + ): + integration.add_error( + "requirements", + ( + f"Integration {integration.domain} no longer has requirements which are" + " incompatible with the Python standard library, remove it from " + "IGNORE_STANDARD_LIBRARY_VIOLATIONS" + ), + ) @cache diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 724f65eafb6..e815a66b4bb 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -3,7 +3,6 @@ from __future__ import annotations from functools import partial -from itertools import chain import json import re from typing import Any @@ -12,7 +11,6 @@ import voluptuous as vol from voluptuous.humanize import humanize_error import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from script.translations import upload from .model import Config, Integration @@ -24,6 +22,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(? str: """Validate that the value is a valid translation. - prevents string with HTML + - prevents strings with single quoted placeholders - prevents combined translations """ value = cv.string_with_no_html(value) + value = string_no_single_quoted_placeholders(value) if RE_COMBINED_REFERENCE.search(value): raise vol.Invalid("the string should not contain combined translations") return str(value) +def string_no_single_quoted_placeholders(value: str) -> str: + """Validate that the value does not contain placeholders inside single quotes.""" + if RE_PLACEHOLDER_IN_SINGLE_QUOTES.search(value): + raise vol.Invalid( + "the string should not contain placeholders inside single quotes" + ) + return value + + def gen_data_entry_schema( *, config: Config, @@ -360,6 +370,11 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: }, slug_validator=translation_key_validator, ), + vol.Optional("conversation"): { + vol.Required("agent"): { + vol.Required("done"): translation_value_validator, + }, + }, } ) @@ -397,49 +412,6 @@ def gen_ha_hardware_schema(config: Config, integration: Integration): ) -def gen_platform_strings_schema(config: Config, integration: Integration) -> vol.Schema: - """Generate platform strings schema like strings.sensor.json. - - Example of valid data: - { - "state": { - "moon__phase": { - "full": "Full" - } - } - } - """ - - def device_class_validator(value: str) -> str: - """Key validator for platform states. - - Platform states are only allowed to provide states for device classes they prefix. - """ - if not value.startswith(f"{integration.domain}__"): - raise vol.Invalid( - f"Device class need to start with '{integration.domain}__'. Key {value} is invalid. See https://developers.home-assistant.io/docs/internationalization/core#stringssensorjson" - ) - - slug_friendly = value.replace("__", "_", 1) - slugged = slugify(slug_friendly) - - if slug_friendly != slugged: - raise vol.Invalid( - f"invalid device class {value}. After domain__, needs to be all lowercase, no spaces." - ) - - return value - - return vol.Schema( - { - vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str, slug_validator=translation_key_validator), - slug_validator=device_class_validator, - ) - } - ) - - ONBOARDING_SCHEMA = vol.Schema( { vol.Required("area"): {str: translation_value_validator}, @@ -508,32 +480,6 @@ def validate_translation_file( # noqa: C901 "name or add exception to ALLOW_NAME_TRANSLATION", ) - platform_string_schema = gen_platform_strings_schema(config, integration) - platform_strings = [integration.path.glob("strings.*.json")] - - if config.specific_integrations: - platform_strings.append(integration.path.glob("translations/*.en.json")) - - for path in chain(*platform_strings): - name = str(path.relative_to(integration.path)) - - try: - strings = json.loads(path.read_text()) - except ValueError as err: - integration.add_error("translations", f"Invalid JSON in {name}: {err}") - continue - - try: - platform_string_schema(strings) - except vol.Invalid as err: - msg = f"Invalid {path.name}: {humanize_error(strings, err)}" - if config.specific_integrations: - integration.add_warning("translations", msg) - else: - integration.add_error("translations", msg) - else: - find_references(strings, path.name, references) - if config.specific_integrations: return diff --git a/script/install_integration_requirements.py b/script/install_integration_requirements.py index 1a87be04f7e..fec893c008a 100644 --- a/script/install_integration_requirements.py +++ b/script/install_integration_requirements.py @@ -18,9 +18,7 @@ def get_arguments() -> argparse.Namespace: "integration", type=valid_integration, help="Integration to target." ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def main() -> int | None: diff --git a/script/lint_and_test.py b/script/lint_and_test.py index 0b0562a0a84..393c5961c7a 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -76,9 +76,9 @@ async def async_exec(*args, display=False): if display: kwargs["stderr"] = asyncio.subprocess.PIPE proc = await asyncio.create_subprocess_exec(*args, **kwargs) - except FileNotFoundError as err: + except FileNotFoundError: printc(FAIL, f"Could not execute {args[0]}. Did you install test requirements?") - raise err + raise if not display: # Readin stdout into log diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index f81f9144f98..45dbed790e6 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -25,9 +25,7 @@ def get_arguments() -> argparse.Namespace: "--integration", type=valid_integration, help="Integration to target." ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def main(): diff --git a/script/scaffold/templates/ruff.toml b/script/scaffold/templates/ruff.toml new file mode 100644 index 00000000000..00a6d7ef849 --- /dev/null +++ b/script/scaffold/templates/ruff.toml @@ -0,0 +1,6 @@ +extend = "../../ruff.toml" + +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] diff --git a/script/split_tests.py b/script/split_tests.py new file mode 100755 index 00000000000..8da03bd749b --- /dev/null +++ b/script/split_tests.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Helper script to split test into n buckets.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass, field +from math import ceil +from pathlib import Path +import subprocess +import sys +from typing import Final + + +class Bucket: + """Class to hold bucket.""" + + def __init__( + self, + ): + """Initialize bucket.""" + self.total_tests = 0 + self._paths: list[str] = [] + + def add(self, part: TestFolder | TestFile) -> None: + """Add tests to bucket.""" + part.add_to_bucket() + self.total_tests += part.total_tests + self._paths.append(str(part.path)) + + def get_paths_line(self) -> str: + """Return paths.""" + return " ".join(self._paths) + "\n" + + +class BucketHolder: + """Class to hold buckets.""" + + def __init__(self, tests_per_bucket: int, bucket_count: int) -> None: + """Initialize bucket holder.""" + self._tests_per_bucket = tests_per_bucket + self._bucket_count = bucket_count + self._buckets: list[Bucket] = [Bucket() for _ in range(bucket_count)] + + def split_tests(self, test_folder: TestFolder) -> None: + """Split tests into buckets.""" + digits = len(str(test_folder.total_tests)) + sorted_tests = sorted( + test_folder.get_all_flatten(), reverse=True, key=lambda x: x.total_tests + ) + for tests in sorted_tests: + print(f"{tests.total_tests:>{digits}} tests in {tests.path}") + if tests.added_to_bucket: + # Already added to bucket + continue + + smallest_bucket = min(self._buckets, key=lambda x: x.total_tests) + if ( + smallest_bucket.total_tests + tests.total_tests < self._tests_per_bucket + ) or isinstance(tests, TestFile): + smallest_bucket.add(tests) + + # verify that all tests are added to a bucket + if not test_folder.added_to_bucket: + raise ValueError("Not all tests are added to a bucket") + + def create_ouput_file(self) -> None: + """Create output file.""" + with open("pytest_buckets.txt", "w") as file: + for idx, bucket in enumerate(self._buckets): + print(f"Bucket {idx+1} has {bucket.total_tests} tests") + file.write(bucket.get_paths_line()) + + +@dataclass +class TestFile: + """Class represents a single test file and the number of tests it has.""" + + total_tests: int + path: Path + added_to_bucket: bool = field(default=False, init=False) + + def add_to_bucket(self) -> None: + """Add test file to bucket.""" + if self.added_to_bucket: + raise ValueError("Already added to bucket") + self.added_to_bucket = True + + def __gt__(self, other: TestFile) -> bool: + """Return if greater than.""" + return self.total_tests > other.total_tests + + +class TestFolder: + """Class to hold a folder with test files and folders.""" + + def __init__(self, path: Path) -> None: + """Initialize test folder.""" + self.path: Final = path + self.children: dict[Path, TestFolder | TestFile] = {} + + @property + def total_tests(self) -> int: + """Return total tests.""" + return sum([test.total_tests for test in self.children.values()]) + + @property + def added_to_bucket(self) -> bool: + """Return if added to bucket.""" + return all(test.added_to_bucket for test in self.children.values()) + + def add_to_bucket(self) -> None: + """Add test file to bucket.""" + if self.added_to_bucket: + raise ValueError("Already added to bucket") + for child in self.children.values(): + child.add_to_bucket() + + def __repr__(self) -> str: + """Return representation.""" + return ( + f"TestFolder(total_tests={self.total_tests}, children={len(self.children)})" + ) + + def add_test_file(self, file: TestFile) -> None: + """Add test file to folder.""" + path = file.path + relative_path = path.relative_to(self.path) + if not relative_path.parts: + raise ValueError("Path is not a child of this folder") + + if len(relative_path.parts) == 1: + self.children[path] = file + return + + child_path = self.path / relative_path.parts[0] + if (child := self.children.get(child_path)) is None: + self.children[child_path] = child = TestFolder(child_path) + elif not isinstance(child, TestFolder): + raise ValueError("Child is not a folder") + child.add_test_file(file) + + def get_all_flatten(self) -> list[TestFolder | TestFile]: + """Return self and all children as flatten list.""" + result: list[TestFolder | TestFile] = [self] + for child in self.children.values(): + if isinstance(child, TestFolder): + result.extend(child.get_all_flatten()) + else: + result.append(child) + return result + + +def collect_tests(path: Path) -> TestFolder: + """Collect all tests.""" + result = subprocess.run( + ["pytest", "--collect-only", "-qq", "-p", "no:warnings", path], + check=False, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + print("Failed to collect tests:") + print(result.stderr) + print(result.stdout) + sys.exit(1) + + folder = TestFolder(path) + + for line in result.stdout.splitlines(): + if not line.strip(): + continue + file_path, _, total_tests = line.partition(": ") + if not path or not total_tests: + print(f"Unexpected line: {line}") + sys.exit(1) + + file = TestFile(int(total_tests), Path(file_path)) + folder.add_test_file(file) + + return folder + + +def main() -> None: + """Execute script.""" + parser = argparse.ArgumentParser(description="Split tests into n buckets.") + + def check_greater_0(value: str) -> int: + ivalue = int(value) + if ivalue <= 0: + raise argparse.ArgumentTypeError( + f"{value} is an invalid. Must be greater than 0" + ) + return ivalue + + parser.add_argument( + "bucket_count", + help="Number of buckets to split tests into", + type=check_greater_0, + ) + parser.add_argument( + "path", + help="Path to the test files to split into buckets", + type=Path, + ) + + arguments = parser.parse_args() + + print("Collecting tests...") + tests = collect_tests(arguments.path) + tests_per_bucket = ceil(tests.total_tests / arguments.bucket_count) + + bucket_holder = BucketHolder(tests_per_bucket, arguments.bucket_count) + print("Splitting tests...") + bucket_holder.split_tests(tests) + + print(f"Total tests: {tests.total_tests}") + print(f"Estimated tests per bucket: {tests_per_bucket}") + + bucket_holder.create_ouput_file() + + +if __name__ == "__main__": + main() diff --git a/script/translations/develop.py b/script/translations/develop.py index 14e3c320c3e..00465e1bc24 100644 --- a/script/translations/develop.py +++ b/script/translations/develop.py @@ -43,7 +43,7 @@ def flatten_translations(translations): if isinstance(v, dict): stack.append(iter(v.items())) break - elif isinstance(v, str): + if isinstance(v, str): common_key = "::".join(key_stack) flattened_translations[common_key] = v key_stack.pop() diff --git a/tests/common.py b/tests/common.py index 0ac0ee4556b..a3af2a3103b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections import OrderedDict from collections.abc import AsyncGenerator, Generator, Mapping, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import UTC, datetime, timedelta @@ -23,6 +22,7 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import pytest +from syrupy import SnapshotAssertion import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader @@ -76,7 +76,6 @@ from homeassistant.helpers import ( translation, ) from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) @@ -95,6 +94,7 @@ from homeassistant.util.json import ( json_loads_array, json_loads_object, ) +from homeassistant.util.signal_type import SignalType from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.uuid as uuid_util import homeassistant.util.yaml.loader as yaml_loader @@ -234,7 +234,7 @@ async def async_test_home_assistant( orig_async_add_job = hass.async_add_job orig_async_add_executor_job = hass.async_add_executor_job - orig_async_create_task = hass.async_create_task + orig_async_create_task_internal = hass.async_create_task_internal orig_tz = dt_util.DEFAULT_TIME_ZONE def async_add_job(target, *args, eager_start: bool = False): @@ -263,18 +263,18 @@ async def async_test_home_assistant( return orig_async_add_executor_job(target, *args) - def async_create_task(coroutine, name=None, eager_start=False): + def async_create_task_internal(coroutine, name=None, eager_start=True): """Create task.""" if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): fut = asyncio.Future() fut.set_result(None) return fut - return orig_async_create_task(coroutine, name, eager_start) + return orig_async_create_task_internal(coroutine, name, eager_start) hass.async_add_job = async_add_job hass.async_add_executor_job = async_add_executor_job - hass.async_create_task = async_create_task + hass.async_create_task_internal = async_create_task_internal hass.data[loader.DATA_CUSTOM_COMPONENTS] = {} @@ -300,7 +300,6 @@ async def async_test_home_assistant( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, hass.config_entries._async_shutdown, - run_immediately=True, ) # Load the registries @@ -354,9 +353,9 @@ async def async_test_home_assistant( hass.set_state(CoreState.running) - @callback - def clear_instance(event): + async def clear_instance(event): """Clear global instance.""" + await asyncio.sleep(0) # Give aiohttp one loop iteration to close INSTANCES.remove(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance) @@ -453,7 +452,7 @@ def async_fire_mqtt_message( mqtt_data: MqttData = hass.data["mqtt"] assert mqtt_data.client - mqtt_data.client._mqtt_handle_message(msg) + mqtt_data.client._async_mqtt_on_message(Mock(), None, msg) fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @@ -588,12 +587,12 @@ def json_round_trip(obj: Any) -> Any: def mock_state_change_event( hass: HomeAssistant, new_state: State, old_state: State | None = None ) -> None: - """Mock state change envent.""" - event_data = {"entity_id": new_state.entity_id, "new_state": new_state} - - if old_state: - event_data["old_state"] = old_state - + """Mock state change event.""" + event_data = { + "entity_id": new_state.entity_id, + "new_state": new_state, + "old_state": old_state, + } hass.bus.fire(EVENT_STATE_CHANGED, event_data, context=new_state.context) @@ -649,7 +648,9 @@ def mock_area_registry( fixture instead. """ registry = ar.AreaRegistry(hass) - registry.areas = mock_entries or OrderedDict() + registry.areas = ar.AreaRegistryItems() + for key, entry in mock_entries.items(): + registry.areas[key] = entry hass.data[ar.DATA_REGISTRY] = registry return registry @@ -671,7 +672,7 @@ def mock_device_registry( fixture instead. """ registry = dr.DeviceRegistry(hass) - registry.devices = dr.DeviceRegistryItems() + registry.devices = dr.ActiveDeviceRegistryItems() registry._device_data = registry.devices.data if mock_entries is None: mock_entries = {} @@ -915,9 +916,7 @@ 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_stop) class MockToggleEntity(entity.ToggleEntity): @@ -1490,7 +1489,7 @@ def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: def capture_events(event: Event) -> None: events.append(event) - hass.bus.async_listen(event_name, capture_events, run_immediately=True) + hass.bus.async_listen(event_name, capture_events) return events @@ -1524,12 +1523,12 @@ class _HA_ANY: _other = _SENTINEL - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Test equal.""" self._other = other return True - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: """Test not equal.""" self._other = other return False @@ -1639,6 +1638,40 @@ def import_and_test_deprecated_constant( assert constant_name in module.__all__ +def import_and_test_deprecated_alias( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Import and test deprecated alias replaced by a value. + + - Import deprecated alias + - Assert value is the same as the replacement + - Assert a warning is logged + - Assert the deprecated alias is included in the modules.__dir__() + - Assert the deprecated alias is included in the modules.__all__() + """ + replacement_name = f"{replacement.__module__}.{replacement.__name__}" + value = import_deprecated_constant(module, alias_name) + assert value == replacement + assert ( + module.__name__, + logging.WARNING, + ( + f"{alias_name} was used from test_constant_deprecation," + f" this is a deprecated alias which will be removed in HA Core {breaks_in_ha_version}. " + f"Use {replacement_name} instead, please report " + "it to the author of the 'test_constant_deprecation' custom integration" + ), + ) in caplog.record_tuples + + # verify deprecated alias is included in dir() + assert alias_name in dir(module) + assert alias_name in module.__all__ + + def help_test_all(module: ModuleType) -> None: """Test module.__all__ is correctly set.""" assert set(module.__all__) == { @@ -1701,3 +1734,22 @@ def setup_test_component_platform( mock_platform(hass, f"test.{domain}", platform, built_in=built_in) return platform + + +async def snapshot_platform( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_entry_id: str, +) -> None: + """Snapshot a platform.""" + entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + assert entity_entries + assert ( + len({entity_entry.domain for entity_entry in entity_entries}) == 1 + ), "Please limit the loaded platforms to 1 platform." + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert entity_entry.disabled_by is None, "Please enable all entities." + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 2f73ee052c1..265a77560f7 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -10,12 +10,12 @@ from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED import pytest from requests.exceptions import ConnectTimeout -from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.components.abode.const import CONF_POLLING, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, 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 @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -45,7 +45,7 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: step_user_result = await flow.async_step_user() - assert step_user_result["type"] == data_entry_flow.FlowResultType.ABORT + assert step_user_result["type"] is FlowResultType.ABORT assert step_user_result["reason"] == "single_instance_allowed" @@ -107,7 +107,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -128,7 +128,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mfa" with patch( @@ -148,7 +148,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: result["flow_id"], user_input={"mfa_code": "123456"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -174,7 +174,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.config_entries.ConfigEntries.async_reload"): @@ -183,7 +183,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: user_input=conf, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index e6e5da35a5e..58e9ccb2c41 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -8,7 +8,6 @@ from jaraco.abode.exceptions import ( Exception as AbodeException, ) -from homeassistant import data_entry_flow from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -19,6 +18,7 @@ from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import setup_platform @@ -82,7 +82,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: patch( "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", return_value={ - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "flow_id": "mock_flow", "step_id": "reauth_confirm", }, diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index afaa5bbef25..a08b894ebb4 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -11,14 +11,8 @@ from tests.common import ( ) -async def init_integration( - hass, forecast=False, unsupported_icon=False -) -> MockConfigEntry: +async def init_integration(hass, unsupported_icon=False) -> MockConfigEntry: """Set up the AccuWeather integration in Home Assistant.""" - options = {} - if forecast: - options["forecast"] = True - entry = MockConfigEntry( domain=DOMAIN, title="Home", @@ -29,7 +23,6 @@ async def init_integration( "longitude": 122.12, "name": "Home", }, - options=options, ) current = load_json_object_fixture("accuweather/current_conditions_data.json") diff --git a/tests/components/accuweather/snapshots/test_diagnostics.ambr b/tests/components/accuweather/snapshots/test_diagnostics.ambr index b3c0c1de752..7477602f3a4 100644 --- a/tests/components/accuweather/snapshots/test_diagnostics.ambr +++ b/tests/components/accuweather/snapshots/test_diagnostics.ambr @@ -7,7 +7,7 @@ 'longitude': '**REDACTED**', 'name': 'Home', }), - 'coordinator_data': dict({ + 'observation_data': dict({ 'ApparentTemperature': dict({ 'Imperial': dict({ 'Unit': 'F', @@ -297,8 +297,6 @@ }), }), }), - 'forecast': list([ - ]), }), }) # --- diff --git a/tests/components/accuweather/snapshots/test_sensor.ambr b/tests/components/accuweather/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..42783f375b0 --- /dev/null +++ b/tests/components/accuweather/snapshots/test_sensor.ambr @@ -0,0 +1,6436 @@ +# serializer version: 1 +# name: test_sensor[sensor.home_air_quality_day_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:air-filter', + 'original_name': 'Air quality day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_1d', + 'unique_id': '0123456-airquality-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 1', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:air-filter', + 'original_name': 'Air quality day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_2d', + 'unique_id': '0123456-airquality-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 2', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:air-filter', + 'original_name': 'Air quality day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_3d', + 'unique_id': '0123456-airquality-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 3', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:air-filter', + 'original_name': 'Air quality day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_4d', + 'unique_id': '0123456-airquality-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 4', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:air-filter', + 'original_name': 'Air quality today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_0d', + 'unique_id': '0123456-airquality-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality today', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_apparent_temperature-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.home_apparent_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': 'Apparent temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'apparent_temperature', + 'unique_id': '0123456-apparenttemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_apparent_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Apparent temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_apparent_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.8', + }) +# --- +# name: test_sensor[sensor.home_cloud_ceiling-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.home_cloud_ceiling', + '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': 'mdi:weather-fog', + 'original_name': 'Cloud ceiling', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_ceiling', + 'unique_id': '0123456-ceiling', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_cloud_ceiling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'distance', + 'friendly_name': 'Home Cloud ceiling', + 'icon': 'mdi:weather-fog', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_cloud_ceiling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3200.0', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover-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.home_cloud_cover', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover', + 'unique_id': '0123456-cloudcover', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover', + 'icon': 'mdi:weather-cloudy', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_1-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.home_cloud_cover_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_1d', + 'unique_id': '0123456-cloudcoverday-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 1', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_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.home_cloud_cover_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_2d', + 'unique_id': '0123456-cloudcoverday-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 2', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_3-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.home_cloud_cover_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_3d', + 'unique_id': '0123456-cloudcoverday-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 3', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_4-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.home_cloud_cover_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_4d', + 'unique_id': '0123456-cloudcoverday-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 4', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_1-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.home_cloud_cover_night_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_1d', + 'unique_id': '0123456-cloudcovernight-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 1', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '63', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_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.home_cloud_cover_night_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_2d', + 'unique_id': '0123456-cloudcovernight-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 2', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_3-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.home_cloud_cover_night_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_3d', + 'unique_id': '0123456-cloudcovernight-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 3', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_4-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.home_cloud_cover_night_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover night 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_4d', + 'unique_id': '0123456-cloudcovernight-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 4', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_today-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.home_cloud_cover_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_0d', + 'unique_id': '0123456-cloudcoverday-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover today', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '58', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_tonight-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.home_cloud_cover_tonight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover tonight', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_0d', + 'unique_id': '0123456-cloudcovernight-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_tonight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover tonight', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_tonight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- +# name: test_sensor[sensor.home_condition_day_1-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.home_condition_day_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': 'Condition day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_1d', + 'unique_id': '0123456-longphraseday-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 1', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Clouds and sun', + }) +# --- +# name: test_sensor[sensor.home_condition_day_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.home_condition_day_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': 'Condition day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_2d', + 'unique_id': '0123456-longphraseday-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 2', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Very warm with a blend of sun and clouds', + }) +# --- +# name: test_sensor[sensor.home_condition_day_3-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.home_condition_day_3', + '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': 'Condition day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_3d', + 'unique_id': '0123456-longphraseday-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 3', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Cooler with partial sunshine', + }) +# --- +# name: test_sensor[sensor.home_condition_day_4-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.home_condition_day_4', + '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': 'Condition day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_4d', + 'unique_id': '0123456-longphraseday-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 4', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Intervals of clouds and sunshine', + }) +# --- +# name: test_sensor[sensor.home_condition_night_1-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.home_condition_night_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': 'Condition night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_1d', + 'unique_id': '0123456-longphrasenight-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 1', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Partly cloudy', + }) +# --- +# name: test_sensor[sensor.home_condition_night_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.home_condition_night_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': 'Condition night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_2d', + 'unique_id': '0123456-longphrasenight-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 2', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Partly cloudy', + }) +# --- +# name: test_sensor[sensor.home_condition_night_3-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.home_condition_night_3', + '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': 'Condition night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_3d', + 'unique_id': '0123456-longphrasenight-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 3', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Mainly clear', + }) +# --- +# name: test_sensor[sensor.home_condition_night_4-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.home_condition_night_4', + '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': 'Condition night 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_4d', + 'unique_id': '0123456-longphrasenight-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 4', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Mostly clear', + }) +# --- +# name: test_sensor[sensor.home_condition_today-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.home_condition_today', + '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': 'Condition today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_0d', + 'unique_id': '0123456-longphraseday-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition today', + }), + 'context': , + 'entity_id': 'sensor.home_condition_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon', + }) +# --- +# name: test_sensor[sensor.home_condition_tonight-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.home_condition_tonight', + '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': 'Condition tonight', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_0d', + 'unique_id': '0123456-longphrasenight-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_tonight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition tonight', + }), + 'context': , + 'entity_id': 'sensor.home_condition_tonight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Partly cloudy', + }) +# --- +# name: test_sensor[sensor.home_dew_point-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.home_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': '0123456-dewpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Dew point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.2', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_1-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.home_grass_pollen_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:grass', + 'original_name': 'Grass pollen day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_1d', + 'unique_id': '0123456-grass-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 1', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_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.home_grass_pollen_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:grass', + 'original_name': 'Grass pollen day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_2d', + 'unique_id': '0123456-grass-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 2', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_3-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.home_grass_pollen_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:grass', + 'original_name': 'Grass pollen day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_3d', + 'unique_id': '0123456-grass-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 3', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_4-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.home_grass_pollen_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:grass', + 'original_name': 'Grass pollen day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_4d', + 'unique_id': '0123456-grass-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 4', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_today-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.home_grass_pollen_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:grass', + 'original_name': 'Grass pollen today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_0d', + 'unique_id': '0123456-grass-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen today', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_1-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.home_hours_of_sun_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_1d', + 'unique_id': '0123456-hoursofsun-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 1', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.4', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_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.home_hours_of_sun_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_2d', + 'unique_id': '0123456-hoursofsun-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 2', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.7', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_3-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.home_hours_of_sun_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_3d', + 'unique_id': '0123456-hoursofsun-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 3', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.4', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_4-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.home_hours_of_sun_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_4d', + 'unique_id': '0123456-hoursofsun-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 4', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.2', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_today-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.home_hours_of_sun_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_0d', + 'unique_id': '0123456-hoursofsun-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun today', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.2', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_1-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.home_mold_pollen_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:blur', + 'original_name': 'Mold pollen day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_1d', + 'unique_id': '0123456-mold-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 1', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_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.home_mold_pollen_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:blur', + 'original_name': 'Mold pollen day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_2d', + 'unique_id': '0123456-mold-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 2', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_3-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.home_mold_pollen_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:blur', + 'original_name': 'Mold pollen day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_3d', + 'unique_id': '0123456-mold-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 3', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_4-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.home_mold_pollen_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:blur', + 'original_name': 'Mold pollen day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_4d', + 'unique_id': '0123456-mold-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 4', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_today-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.home_mold_pollen_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:blur', + 'original_name': 'Mold pollen today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_0d', + 'unique_id': '0123456-mold-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen today', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_precipitation-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.home_precipitation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'precipitation', + 'unique_id': '0123456-precipitation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_precipitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'Home Precipitation', + 'state_class': , + 'type': None, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_precipitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[sensor.home_pressure_tendency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'falling', + 'rising', + 'steady', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_pressure_tendency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:gauge', + 'original_name': 'Pressure tendency', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure_tendency', + 'unique_id': '0123456-pressuretendency', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_pressure_tendency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Pressure tendency', + 'icon': 'mdi:gauge', + 'options': list([ + 'falling', + 'rising', + 'steady', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_pressure_tendency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'falling', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_1-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.home_ragweed_pollen_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sprout', + 'original_name': 'Ragweed pollen day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_1d', + 'unique_id': '0123456-ragweed-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 1', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_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.home_ragweed_pollen_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sprout', + 'original_name': 'Ragweed pollen day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_2d', + 'unique_id': '0123456-ragweed-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 2', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_3-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.home_ragweed_pollen_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sprout', + 'original_name': 'Ragweed pollen day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_3d', + 'unique_id': '0123456-ragweed-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 3', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_4-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.home_ragweed_pollen_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sprout', + 'original_name': 'Ragweed pollen day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_4d', + 'unique_id': '0123456-ragweed-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 4', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_today-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.home_ragweed_pollen_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sprout', + 'original_name': 'Ragweed pollen today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_0d', + 'unique_id': '0123456-ragweed-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen today', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature-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.home_realfeel_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': 'RealFeel temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature', + 'unique_id': '0123456-realfeeltemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_1-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.home_realfeel_temperature_max_day_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': 'RealFeel temperature max day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_1d', + 'unique_id': '0123456-realfeeltemperaturemax-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.9', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_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.home_realfeel_temperature_max_day_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': 'RealFeel temperature max day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_2d', + 'unique_id': '0123456-realfeeltemperaturemax-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.6', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_3-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.home_realfeel_temperature_max_day_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': 'RealFeel temperature max day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_3d', + 'unique_id': '0123456-realfeeltemperaturemax-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.5', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_4-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.home_realfeel_temperature_max_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature max day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_4d', + 'unique_id': '0123456-realfeeltemperaturemax-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_today-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.home_realfeel_temperature_max_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature max today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_0d', + 'unique_id': '0123456-realfeeltemperaturemax-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.8', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_1-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.home_realfeel_temperature_min_day_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': 'RealFeel temperature min day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_1d', + 'unique_id': '0123456-realfeeltemperaturemin-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.8', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_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.home_realfeel_temperature_min_day_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': 'RealFeel temperature min day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_2d', + 'unique_id': '0123456-realfeeltemperaturemin-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.7', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_3-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.home_realfeel_temperature_min_day_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': 'RealFeel temperature min day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_3d', + 'unique_id': '0123456-realfeeltemperaturemin-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_4-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.home_realfeel_temperature_min_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature min day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_4d', + 'unique_id': '0123456-realfeeltemperaturemin-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.3', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_today-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.home_realfeel_temperature_min_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature min today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_0d', + 'unique_id': '0123456-realfeeltemperaturemin-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade-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.home_realfeel_temperature_shade', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature shade', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade', + 'unique_id': '0123456-realfeeltemperatureshade', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_1-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.home_realfeel_temperature_shade_max_day_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': 'RealFeel temperature shade max day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_1d', + 'unique_id': '0123456-realfeeltemperatureshademax-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_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.home_realfeel_temperature_shade_max_day_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': 'RealFeel temperature shade max day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_2d', + 'unique_id': '0123456-realfeeltemperatureshademax-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_3-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.home_realfeel_temperature_shade_max_day_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': 'RealFeel temperature shade max day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_3d', + 'unique_id': '0123456-realfeeltemperatureshademax-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.5', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_4-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.home_realfeel_temperature_shade_max_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature shade max day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_4d', + 'unique_id': '0123456-realfeeltemperatureshademax-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19.5', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_today-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.home_realfeel_temperature_shade_max_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature shade max today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_0d', + 'unique_id': '0123456-realfeeltemperatureshademax-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_1-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.home_realfeel_temperature_shade_min_day_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': 'RealFeel temperature shade min day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_1d', + 'unique_id': '0123456-realfeeltemperatureshademin-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.8', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_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.home_realfeel_temperature_shade_min_day_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': 'RealFeel temperature shade min day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_2d', + 'unique_id': '0123456-realfeeltemperatureshademin-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.7', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_3-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.home_realfeel_temperature_shade_min_day_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': 'RealFeel temperature shade min day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_3d', + 'unique_id': '0123456-realfeeltemperatureshademin-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_4-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.home_realfeel_temperature_shade_min_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature shade min day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_4d', + 'unique_id': '0123456-realfeeltemperatureshademin-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.3', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_today-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.home_realfeel_temperature_shade_min_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature shade min today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_0d', + 'unique_id': '0123456-realfeeltemperatureshademin-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_1-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.home_solar_irradiance_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_1d', + 'unique_id': '0123456-solarirradianceday-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 1', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_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.home_solar_irradiance_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_2d', + 'unique_id': '0123456-solarirradianceday-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 2', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_3-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.home_solar_irradiance_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_3d', + 'unique_id': '0123456-solarirradianceday-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 3', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_4-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.home_solar_irradiance_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_4d', + 'unique_id': '0123456-solarirradianceday-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 4', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_1-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.home_solar_irradiance_night_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_1d', + 'unique_id': '0123456-solarirradiancenight-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 1', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '271.6', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_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.home_solar_irradiance_night_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_2d', + 'unique_id': '0123456-solarirradiancenight-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 2', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '271.6', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_3-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.home_solar_irradiance_night_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_3d', + 'unique_id': '0123456-solarirradiancenight-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 3', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_4-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.home_solar_irradiance_night_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance night 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_4d', + 'unique_id': '0123456-solarirradiancenight-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 4', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '276.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_today-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.home_solar_irradiance_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_0d', + 'unique_id': '0123456-solarirradianceday-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance today', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_tonight-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.home_solar_irradiance_tonight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance tonight', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_0d', + 'unique_id': '0123456-solarirradiancenight-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_tonight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance tonight', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_tonight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '271.6', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_1-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.home_thunderstorm_probability_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_1d', + 'unique_id': '0123456-thunderstormprobabilityday-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 1', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_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.home_thunderstorm_probability_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_2d', + 'unique_id': '0123456-thunderstormprobabilityday-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 2', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_3-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.home_thunderstorm_probability_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_3d', + 'unique_id': '0123456-thunderstormprobabilityday-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 3', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_4-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.home_thunderstorm_probability_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_4d', + 'unique_id': '0123456-thunderstormprobabilityday-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 4', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_1-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.home_thunderstorm_probability_night_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_1d', + 'unique_id': '0123456-thunderstormprobabilitynight-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 1', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_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.home_thunderstorm_probability_night_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_2d', + 'unique_id': '0123456-thunderstormprobabilitynight-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 2', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_3-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.home_thunderstorm_probability_night_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_3d', + 'unique_id': '0123456-thunderstormprobabilitynight-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 3', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_4-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.home_thunderstorm_probability_night_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability night 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_4d', + 'unique_id': '0123456-thunderstormprobabilitynight-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 4', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_today-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.home_thunderstorm_probability_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_0d', + 'unique_id': '0123456-thunderstormprobabilityday-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability today', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_tonight-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.home_thunderstorm_probability_tonight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability tonight', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_0d', + 'unique_id': '0123456-thunderstormprobabilitynight-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_tonight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability tonight', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_tonight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_1-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.home_tree_pollen_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:tree-outline', + 'original_name': 'Tree pollen day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_1d', + 'unique_id': '0123456-tree-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Tree pollen day 1', + 'icon': 'mdi:tree-outline', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_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.home_tree_pollen_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:tree-outline', + 'original_name': 'Tree pollen day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_2d', + 'unique_id': '0123456-tree-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Tree pollen day 2', + 'icon': 'mdi:tree-outline', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_3-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.home_tree_pollen_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:tree-outline', + 'original_name': 'Tree pollen day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_3d', + 'unique_id': '0123456-tree-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Tree pollen day 3', + 'icon': 'mdi:tree-outline', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_4-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.home_tree_pollen_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:tree-outline', + 'original_name': 'Tree pollen day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_4d', + 'unique_id': '0123456-tree-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Tree pollen day 4', + 'icon': 'mdi:tree-outline', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_today-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.home_tree_pollen_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:tree-outline', + 'original_name': 'Tree pollen today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_0d', + 'unique_id': '0123456-tree-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Tree pollen today', + 'icon': 'mdi:tree-outline', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_uv_index-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.home_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': '0123456-uvindex', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index', + 'icon': 'mdi:weather-sunny', + 'level': 'High', + 'state_class': , + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_1-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.home_uv_index_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_1d', + 'unique_id': '0123456-uvindex-1', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index day 1', + 'icon': 'mdi:weather-sunny', + 'level': 'high', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_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.home_uv_index_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_2d', + 'unique_id': '0123456-uvindex-2', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index day 2', + 'icon': 'mdi:weather-sunny', + 'level': 'high', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_3-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.home_uv_index_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_3d', + 'unique_id': '0123456-uvindex-3', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index day 3', + 'icon': 'mdi:weather-sunny', + 'level': 'high', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_4-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.home_uv_index_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_4d', + 'unique_id': '0123456-uvindex-4', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index day 4', + 'icon': 'mdi:weather-sunny', + 'level': 'high', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_sensor[sensor.home_uv_index_today-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.home_uv_index_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_0d', + 'unique_id': '0123456-uvindex-0', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index today', + 'icon': 'mdi:weather-sunny', + 'level': 'moderate', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor[sensor.home_wet_bulb_temperature-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.home_wet_bulb_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': 'Wet bulb temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wet_bulb_temperature', + 'unique_id': '0123456-wetbulbtemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wet_bulb_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Wet bulb temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wet_bulb_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_sensor[sensor.home_wind_chill_temperature-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.home_wind_chill_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': 'Wind chill temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_chill_temperature', + 'unique_id': '0123456-windchilltemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_chill_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Wind chill temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_chill_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed-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.home_wind_gust_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed', + 'unique_id': '0123456-windgust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'friendly_name': 'Home Wind gust speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.3', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_1-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.home_wind_gust_speed_day_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': 'Wind gust speed day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_1d', + 'unique_id': '0123456-windgustday-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'NW', + 'friendly_name': 'Home Wind gust speed day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_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.home_wind_gust_speed_day_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': 'Wind gust speed day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_2d', + 'unique_id': '0123456-windgustday-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSW', + 'friendly_name': 'Home Wind gust speed day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.1', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_3-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.home_wind_gust_speed_day_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': 'Wind gust speed day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_3d', + 'unique_id': '0123456-windgustday-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.1', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_4-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.home_wind_gust_speed_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_4d', + 'unique_id': '0123456-windgustday-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_1-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.home_wind_gust_speed_night_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': 'Wind gust speed night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_1d', + 'unique_id': '0123456-windgustnight-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'S', + 'friendly_name': 'Home Wind gust speed night 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_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.home_wind_gust_speed_night_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': 'Wind gust speed night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_2d', + 'unique_id': '0123456-windgustnight-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed night 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_3-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.home_wind_gust_speed_night_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': 'Wind gust speed night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_3d', + 'unique_id': '0123456-windgustnight-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed night 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_4-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.home_wind_gust_speed_night_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed night 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_4d', + 'unique_id': '0123456-windgustnight-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed night 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_today-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.home_wind_gust_speed_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_0d', + 'unique_id': '0123456-windgustday-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'S', + 'friendly_name': 'Home Wind gust speed today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.6', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_tonight-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.home_wind_gust_speed_tonight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed tonight', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_0d', + 'unique_id': '0123456-windgustnight-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_tonight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WSW', + 'friendly_name': 'Home Wind gust speed tonight', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_tonight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_sensor[sensor.home_wind_speed-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.home_wind_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed', + 'unique_id': '0123456-wind', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'friendly_name': 'Home Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.5', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_1-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.home_wind_speed_day_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': 'Wind speed day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_1d', + 'unique_id': '0123456-windday-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WNW', + 'friendly_name': 'Home Wind speed day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.3', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_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.home_wind_speed_day_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': 'Wind speed day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_2d', + 'unique_id': '0123456-windday-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSW', + 'friendly_name': 'Home Wind speed day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.7', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_3-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.home_wind_speed_day_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': 'Wind speed day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_3d', + 'unique_id': '0123456-windday-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WNW', + 'friendly_name': 'Home Wind speed day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_4-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.home_wind_speed_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_4d', + 'unique_id': '0123456-windday-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_1-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.home_wind_speed_night_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': 'Wind speed night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_1d', + 'unique_id': '0123456-windnight-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSE', + 'friendly_name': 'Home Wind speed night 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.4', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_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.home_wind_speed_night_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': 'Wind speed night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_2d', + 'unique_id': '0123456-windnight-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed night 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.3', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_3-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.home_wind_speed_night_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': 'Wind speed night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_3d', + 'unique_id': '0123456-windnight-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed night 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.1', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_4-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.home_wind_speed_night_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed night 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_4d', + 'unique_id': '0123456-windnight-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed night 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.3', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_today-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.home_wind_speed_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_0d', + 'unique_id': '0123456-windday-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSE', + 'friendly_name': 'Home Wind speed today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_tonight-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.home_wind_speed_tonight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed tonight', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_0d', + 'unique_id': '0123456-windnight-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_tonight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WNW', + 'friendly_name': 'Home Wind speed tonight', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_tonight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.4', + }) +# --- diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr index 081e7bf595a..1542d22aa7b 100644 --- a/tests/components/accuweather/snapshots/test_weather.ambr +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -1,158 +1,4 @@ # serializer version: 1 -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 58, - 'condition': 'lightning-rainy', - 'datetime': '2020-07-26T05:00:00+00:00', - 'precipitation': 2.5, - 'precipitation_probability': 60, - 'temperature': 29.5, - 'templow': 15.4, - 'uv_index': 5, - 'wind_bearing': 166, - 'wind_gust_speed': 29.6, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 52, - 'condition': 'partlycloudy', - 'datetime': '2020-07-27T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 26.2, - 'templow': 15.9, - 'uv_index': 7, - 'wind_bearing': 297, - 'wind_gust_speed': 14.8, - 'wind_speed': 9.3, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 65, - 'condition': 'partlycloudy', - 'datetime': '2020-07-28T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 31.7, - 'templow': 16.8, - 'uv_index': 7, - 'wind_bearing': 198, - 'wind_gust_speed': 24.1, - 'wind_speed': 16.7, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 45, - 'condition': 'partlycloudy', - 'datetime': '2020-07-29T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 9, - 'temperature': 24.0, - 'templow': 11.7, - 'uv_index': 6, - 'wind_bearing': 293, - 'wind_gust_speed': 24.1, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 22.2, - 'cloud_coverage': 50, - 'condition': 'partlycloudy', - 'datetime': '2020-07-30T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 1, - 'temperature': 21.4, - 'templow': 12.2, - 'uv_index': 7, - 'wind_bearing': 280, - 'wind_gust_speed': 27.8, - 'wind_speed': 18.5, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.home': dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 58, - 'condition': 'lightning-rainy', - 'datetime': '2020-07-26T05:00:00+00:00', - 'precipitation': 2.5, - 'precipitation_probability': 60, - 'temperature': 29.5, - 'templow': 15.4, - 'uv_index': 5, - 'wind_bearing': 166, - 'wind_gust_speed': 29.6, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 52, - 'condition': 'partlycloudy', - 'datetime': '2020-07-27T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 26.2, - 'templow': 15.9, - 'uv_index': 7, - 'wind_bearing': 297, - 'wind_gust_speed': 14.8, - 'wind_speed': 9.3, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 65, - 'condition': 'partlycloudy', - 'datetime': '2020-07-28T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 31.7, - 'templow': 16.8, - 'uv_index': 7, - 'wind_bearing': 198, - 'wind_gust_speed': 24.1, - 'wind_speed': 16.7, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 45, - 'condition': 'partlycloudy', - 'datetime': '2020-07-29T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 9, - 'temperature': 24.0, - 'templow': 11.7, - 'uv_index': 6, - 'wind_bearing': 293, - 'wind_gust_speed': 24.1, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 22.2, - 'cloud_coverage': 50, - 'condition': 'partlycloudy', - 'datetime': '2020-07-30T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 1, - 'temperature': 21.4, - 'templow': 12.2, - 'uv_index': 7, - 'wind_bearing': 280, - 'wind_gust_speed': 27.8, - 'wind_speed': 18.5, - }), - ]), - }), - }) -# --- # name: test_forecast_service[get_forecast] dict({ 'forecast': list([ @@ -455,3 +301,67 @@ }), ]) # --- +# name: test_weather[weather.home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.home', + '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': 'accuweather', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '0123456', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather[weather.home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'apparent_temperature': 22.8, + 'attribution': 'Data provided by AccuWeather', + 'cloud_coverage': 10, + 'dew_point': 16.2, + 'friendly_name': 'Home', + 'humidity': 67, + 'precipitation_unit': , + 'pressure': 1012.0, + 'pressure_unit': , + 'supported_features': , + 'temperature': 22.6, + 'temperature_unit': , + 'uv_index': 6, + 'visibility': 16.1, + 'visibility_unit': , + 'wind_bearing': 180, + 'wind_gust_speed': 20.3, + 'wind_speed': 14.5, + 'wind_speed_unit': , + }), + 'context': , + 'entity_id': 'weather.home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'sunny', + }) +# --- diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index c9d95c34b7c..07b126e0856 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,14 +1,14 @@ """Define tests for the AccuWeather config flow.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError -from homeassistant import data_entry_flow -from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN +from homeassistant.components.accuweather.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_json_object_fixture @@ -26,7 +26,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -113,7 +113,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -134,58 +134,9 @@ async def test_create_entry(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "abcd" assert result["data"][CONF_NAME] == "abcd" assert result["data"][CONF_LATITUDE] == 55.55 assert result["data"][CONF_LONGITUDE] == 122.12 assert result["data"][CONF_API_KEY] == "32-character-string-1234567890qw" - - -async def test_options_flow(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="123456", - data=VALID_CONFIG, - ) - 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" - ), - ), - 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() - - result = await hass.config_entries.options.async_init(config_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_FORECAST: True} - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_FORECAST: True} - - await hass.async_block_till_done() - assert await hass.config_entries.async_unload(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 ab77fc337d0..593cde0f0a3 100644 --- a/tests/components/accuweather/test_diagnostics.py +++ b/tests/components/accuweather/test_diagnostics.py @@ -6,7 +6,6 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_json_object_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -19,12 +18,6 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" entry = await init_integration(hass) - coordinator_data = load_json_object_fixture( - "current_conditions_data.json", "accuweather" - ) - - coordinator_data["forecast"] = [] - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == snapshot diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index bb5b67e7918..08ad4a66dec 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -1,11 +1,14 @@ """Test init of AccuWeather integration.""" -from datetime import timedelta from unittest.mock import patch from accuweather import ApiError -from homeassistant.components.accuweather.const import DOMAIN +from homeassistant.components.accuweather.const import ( + DOMAIN, + UPDATE_INTERVAL_DAILY_FORECAST, + UPDATE_INTERVAL_OBSERVATION, +) from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE @@ -76,30 +79,8 @@ async def test_update_interval(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.LOADED - current = load_json_object_fixture("accuweather/current_conditions_data.json") - future = utcnow() + timedelta(minutes=40) - - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ) as mock_current: - assert mock_current.call_count == 0 - - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert mock_current.call_count == 1 - - -async def test_update_interval_forecast(hass: HomeAssistant) -> None: - """Test correct update interval when forecast is True.""" - entry = await init_integration(hass, forecast=True) - - assert entry.state is ConfigEntryState.LOADED - current = load_json_object_fixture("accuweather/current_conditions_data.json") forecast = load_json_array_fixture("accuweather/forecast_data.json") - future = utcnow() + timedelta(minutes=80) with ( patch( @@ -114,10 +95,14 @@ async def test_update_interval_forecast(hass: HomeAssistant) -> None: assert mock_current.call_count == 0 assert mock_forecast.call_count == 0 - async_fire_time_changed(hass, future) + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_OBSERVATION) await hass.async_block_till_done() assert mock_current.call_count == 1 + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST) + await hass.async_block_till_done() + assert mock_forecast.call_count == 1 diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 8e6e01a4578..127e4d74cd8 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -3,29 +3,20 @@ from datetime import timedelta from unittest.mock import PropertyMock, patch -from homeassistant.components.accuweather.const import ATTRIBUTION -from homeassistant.components.sensor import ( - ATTR_OPTIONS, - ATTR_STATE_CLASS, - SensorDeviceClass, - SensorStateClass, -) +from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp.client_exceptions import ClientConnectorError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.accuweather.const import UPDATE_INTERVAL_DAILY_FORECAST from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_PARTS_PER_CUBIC_METER, - PERCENTAGE, STATE_UNAVAILABLE, - UV_INDEX, - UnitOfIrradiance, + Platform, UnitOfLength, UnitOfSpeed, UnitOfTemperature, - UnitOfTime, - UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -39,520 +30,20 @@ from tests.common import ( async_fire_time_changed, load_json_array_fixture, load_json_object_fixture, + snapshot_platform, ) -async def test_sensor_without_forecast( +async def test_sensor( hass: HomeAssistant, entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: - """Test states of the sensor without forecast.""" - await init_integration(hass) - - state = hass.states.get("sensor.home_cloud_ceiling") - assert state - assert state.state == "3200.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - - entry = entity_registry.async_get("sensor.home_cloud_ceiling") - assert entry - assert entry.unique_id == "0123456-ceiling" - assert entry.options["sensor"] == {"suggested_display_precision": 0} - - state = hass.states.get("sensor.home_precipitation") - assert state - assert state.state == "0.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR - ) - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get("type") is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_DEVICE_CLASS) - == SensorDeviceClass.PRECIPITATION_INTENSITY - ) - - entry = entity_registry.async_get("sensor.home_precipitation") - assert entry - assert entry.unique_id == "0123456-precipitation" - - state = hass.states.get("sensor.home_pressure_tendency") - assert state - assert state.state == "falling" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:gauge" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_OPTIONS) == ["falling", "rising", "steady"] - - entry = entity_registry.async_get("sensor.home_pressure_tendency") - assert entry - assert entry.unique_id == "0123456-pressuretendency" - assert entry.translation_key == "pressure_tendency" - - state = hass.states.get("sensor.home_realfeel_temperature") - assert state - assert state.state == "25.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_realfeel_temperature") - assert entry - assert entry.unique_id == "0123456-realfeeltemperature" - - state = hass.states.get("sensor.home_uv_index") - assert state - assert state.state == "6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX - assert state.attributes.get("level") == "High" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_uv_index") - assert entry - assert entry.unique_id == "0123456-uvindex" - - state = hass.states.get("sensor.home_apparent_temperature") - assert state - assert state.state == "22.8" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_apparent_temperature") - assert entry - assert entry.unique_id == "0123456-apparenttemperature" - - state = hass.states.get("sensor.home_cloud_cover") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_cloud_cover") - assert entry - assert entry.unique_id == "0123456-cloudcover" - - state = hass.states.get("sensor.home_dew_point") - assert state - assert state.state == "16.2" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_dew_point") - assert entry - assert entry.unique_id == "0123456-dewpoint" - - state = hass.states.get("sensor.home_realfeel_temperature_shade") - assert state - assert state.state == "21.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_realfeel_temperature_shade") - assert entry - assert entry.unique_id == "0123456-realfeeltemperatureshade" - - state = hass.states.get("sensor.home_wet_bulb_temperature") - assert state - assert state.state == "18.6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_wet_bulb_temperature") - assert entry - assert entry.unique_id == "0123456-wetbulbtemperature" - - state = hass.states.get("sensor.home_wind_chill_temperature") - assert state - assert state.state == "22.8" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_wind_chill_temperature") - assert entry - assert entry.unique_id == "0123456-windchilltemperature" - - state = hass.states.get("sensor.home_wind_gust_speed") - assert state - assert state.state == "20.3" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_gust_speed") - assert entry - assert entry.unique_id == "0123456-windgust" - - state = hass.states.get("sensor.home_wind_speed") - assert state - assert state.state == "14.5" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_speed") - assert entry - assert entry.unique_id == "0123456-wind" - - -async def test_sensor_with_forecast( - hass: HomeAssistant, - entity_registry_enabled_by_default: None, - entity_registry: er.EntityRegistry, -) -> None: - """Test states of the sensor with forecast.""" - await init_integration(hass, forecast=True) - - state = hass.states.get("sensor.home_hours_of_sun_today") - assert state - assert state.state == "7.2" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.HOURS - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_hours_of_sun_today") - assert entry - assert entry.unique_id == "0123456-hoursofsun-0" - - state = hass.states.get("sensor.home_realfeel_temperature_max_today") - assert state - assert state.state == "29.8" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_realfeel_temperature_max_today") - assert entry - - state = hass.states.get("sensor.home_realfeel_temperature_min_today") - assert state - assert state.state == "15.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_realfeel_temperature_min_today") - assert entry - assert entry.unique_id == "0123456-realfeeltemperaturemin-0" - - state = hass.states.get("sensor.home_thunderstorm_probability_today") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_thunderstorm_probability_today") - assert entry - assert entry.unique_id == "0123456-thunderstormprobabilityday-0" - - state = hass.states.get("sensor.home_thunderstorm_probability_tonight") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_thunderstorm_probability_tonight") - assert entry - assert entry.unique_id == "0123456-thunderstormprobabilitynight-0" - - state = hass.states.get("sensor.home_uv_index_today") - assert state - assert state.state == "5" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX - assert state.attributes.get("level") == "moderate" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_uv_index_today") - assert entry - assert entry.unique_id == "0123456-uvindex-0" - - state = hass.states.get("sensor.home_air_quality_today") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "good", - "hazardous", - "high", - "low", - "moderate", - "unhealthy", - ] - - state = hass.states.get("sensor.home_cloud_cover_today") - assert state - assert state.state == "58" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_cloud_cover_today") - assert entry - assert entry.unique_id == "0123456-cloudcoverday-0" - - state = hass.states.get("sensor.home_cloud_cover_tonight") - assert state - assert state.state == "65" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_cloud_cover_tonight") - assert entry - - state = hass.states.get("sensor.home_grass_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:grass" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_grass_pollen_today") - assert entry - assert entry.unique_id == "0123456-grass-0" - - state = hass.states.get("sensor.home_mold_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:blur" - - entry = entity_registry.async_get("sensor.home_mold_pollen_today") - assert entry - assert entry.unique_id == "0123456-mold-0" - - state = hass.states.get("sensor.home_ragweed_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:sprout" - - entry = entity_registry.async_get("sensor.home_ragweed_pollen_today") - assert entry - assert entry.unique_id == "0123456-ragweed-0" - - state = hass.states.get("sensor.home_realfeel_temperature_shade_max_today") - assert state - assert state.state == "28.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get( - "sensor.home_realfeel_temperature_shade_max_today" - ) - assert entry - assert entry.unique_id == "0123456-realfeeltemperatureshademax-0" - - state = hass.states.get("sensor.home_realfeel_temperature_shade_min_today") - assert state - assert state.state == "15.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - - entry = entity_registry.async_get( - "sensor.home_realfeel_temperature_shade_min_today" - ) - assert entry - assert entry.unique_id == "0123456-realfeeltemperatureshademin-0" - - state = hass.states.get("sensor.home_tree_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_tree_pollen_today") - assert entry - assert entry.unique_id == "0123456-tree-0" - - state = hass.states.get("sensor.home_wind_speed_today") - assert state - assert state.state == "13.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "SSE" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_speed_today") - assert entry - assert entry.unique_id == "0123456-windday-0" - - state = hass.states.get("sensor.home_wind_speed_tonight") - assert state - assert state.state == "7.4" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "WNW" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_speed_tonight") - assert entry - assert entry.unique_id == "0123456-windnight-0" - - state = hass.states.get("sensor.home_wind_gust_speed_today") - assert state - assert state.state == "29.6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "S" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_gust_speed_today") - assert entry - assert entry.unique_id == "0123456-windgustday-0" - - state = hass.states.get("sensor.home_wind_gust_speed_tonight") - assert state - assert state.state == "18.5" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "WSW" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_gust_speed_tonight") - assert entry - assert entry.unique_id == "0123456-windgustnight-0" - - entry = entity_registry.async_get("sensor.home_air_quality_today") - assert entry - assert entry.unique_id == "0123456-airquality-0" - - state = hass.states.get("sensor.home_solar_irradiance_today") - assert state - assert state.state == "7447.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfIrradiance.WATTS_PER_SQUARE_METER - ) - - entry = entity_registry.async_get("sensor.home_solar_irradiance_today") - assert entry - assert entry.unique_id == "0123456-solarirradianceday-0" - - state = hass.states.get("sensor.home_solar_irradiance_tonight") - assert state - assert state.state == "271.6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfIrradiance.WATTS_PER_SQUARE_METER - ) - - entry = entity_registry.async_get("sensor.home_solar_irradiance_tonight") - assert entry - assert entry.unique_id == "0123456-solarirradiancenight-0" - - state = hass.states.get("sensor.home_condition_today") - assert state - assert ( - state.state - == "Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon" - ) - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - - entry = entity_registry.async_get("sensor.home_condition_today") - assert entry - assert entry.unique_id == "0123456-longphraseday-0" - - state = hass.states.get("sensor.home_condition_tonight") - assert state - assert state.state == "Partly cloudy" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - - entry = entity_registry.async_get("sensor.home_condition_tonight") - assert entry - assert entry.unique_id == "0123456-longphrasenight-0" + """Test states of the sensor.""" + with patch("homeassistant.components.accuweather.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: @@ -599,24 +90,88 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == "3200.0" +@pytest.mark.parametrize( + "exception", + [ + ApiError, + ConnectionError, + ClientConnectorError, + InvalidApiKeyError, + RequestsExceededError, + ], +) +async def test_availability_forecast(hass: HomeAssistant, exception: Exception) -> None: + """Ensure that we mark the entities unavailable correctly when service is offline.""" + current = load_json_object_fixture("accuweather/current_conditions_data.json") + forecast = load_json_array_fixture("accuweather/forecast_data.json") + entity_id = "sensor.home_hours_of_sun_day_2" + + await init_integration(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "5.7" + + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + side_effect=exception, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), + ): + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + 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, + ), + ): + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST * 2) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "5.7" + + async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeassistant/update_entity.""" - await init_integration(hass, forecast=True) + await init_integration(hass) await async_setup_component(hass, "homeassistant", {}) 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, @@ -629,8 +184,7 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: ["sensor.home_cloud_ceiling"]}, blocking=True, ) - assert mock_current.call_count == 1 - assert mock_forecast.call_count == 1 + assert mock_current.call_count == 1 async def test_sensor_imperial_units(hass: HomeAssistant) -> None: diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py index 6321071eaa5..562c572c830 100644 --- a/tests/components/accuweather/test_system_health.py +++ b/tests/components/accuweather/test_system_health.py @@ -5,6 +5,7 @@ from unittest.mock import Mock from aiohttp import ClientError +from homeassistant.components.accuweather import AccuWeatherData from homeassistant.components.accuweather.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -23,8 +24,10 @@ async def test_accuweather_system_health( await hass.async_block_till_done() hass.data[DOMAIN] = {} - hass.data[DOMAIN]["0123xyz"] = {} - hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="42")) + hass.data[DOMAIN]["0123xyz"] = AccuWeatherData( + coordinator_observation=Mock(accuweather=Mock(requests_remaining="42")), + coordinator_daily_forecast=Mock(), + ) info = await get_system_health_info(hass, DOMAIN) @@ -48,8 +51,10 @@ async def test_accuweather_system_health_fail( await hass.async_block_till_done() hass.data[DOMAIN] = {} - hass.data[DOMAIN]["0123xyz"] = {} - hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="0")) + hass.data[DOMAIN]["0123xyz"] = AccuWeatherData( + coordinator_observation=Mock(accuweather=Mock(requests_remaining="0")), + coordinator_daily_forecast=Mock(), + ) info = await get_system_health_info(hass, DOMAIN) diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 0b9d3e28fb2..d97a5d3da3c 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -7,30 +7,14 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.accuweather.const import ATTRIBUTION +from homeassistant.components.accuweather.const import UPDATE_INTERVAL_DAILY_FORECAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_WEATHER_APPARENT_TEMPERATURE, - ATTR_WEATHER_CLOUD_COVERAGE, - ATTR_WEATHER_DEW_POINT, - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_UV_INDEX, - ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_GUST_SPEED, - ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -42,34 +26,18 @@ from tests.common import ( async_fire_time_changed, load_json_array_fixture, load_json_object_fixture, + snapshot_platform, ) from tests.typing import WebSocketGenerator -async def test_weather(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_weather( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> 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 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 ATTR_SUPPORTED_FEATURES not in state.attributes - - entry = entity_registry.async_get("weather.home") - assert entry - assert entry.unique_id == "0123456" + with patch("homeassistant.components.accuweather.PLATFORMS", [Platform.WEATHER]): + entry = await init_integration(hass) + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: @@ -118,22 +86,17 @@ async def test_availability(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeassistant/update_entity.""" - await init_integration(hass, forecast=True) + await init_integration(hass) await async_setup_component(hass, "homeassistant", {}) 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, @@ -147,12 +110,11 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: blocking=True, ) assert mock_current.call_count == 1 - assert mock_forecast.call_count == 1 async def test_unsupported_condition_icon_data(hass: HomeAssistant) -> None: """Test with unsupported condition icon data.""" - await init_integration(hass, forecast=True, unsupported_icon=True) + await init_integration(hass, unsupported_icon=True) state = hass.states.get("weather.home") assert state.attributes.get(ATTR_FORECAST_CONDITION) is None @@ -171,7 +133,7 @@ async def test_forecast_service( service: str, ) -> None: """Test multiple forecast.""" - await init_integration(hass, forecast=True) + await init_integration(hass) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -195,7 +157,7 @@ async def test_forecast_subscription( """Test multiple forecast.""" client = await hass_ws_client(hass) - await init_integration(hass, forecast=True) + await init_integration(hass) await client.send_json_auto_id( { @@ -235,7 +197,7 @@ async def test_forecast_subscription( return_value=10, ), ): - freezer.tick(timedelta(minutes=80) + timedelta(seconds=1)) + freezer.tick(UPDATE_INTERVAL_DAILY_FORECAST + timedelta(seconds=1)) await hass.async_block_till_done() msg = await client.receive_json() diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index c39470ebbb6..5227d283f25 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch import aiopulse import pytest -from homeassistant import data_entry_flow from homeassistant.components.acmeda.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -49,7 +49,7 @@ async def test_show_form_no_hubs(hass: HomeAssistant, mock_hub_discover) -> None DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Check we performed the discovery @@ -70,7 +70,7 @@ async def test_show_form_one_hub( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_1.id assert result["result"].data == { CONF_HOST: DUMMY_HOST1, @@ -95,7 +95,7 @@ async def test_show_form_two_hubs(hass: HomeAssistant, mock_hub_discover) -> Non DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Check we performed the discovery @@ -123,7 +123,7 @@ async def test_create_second_entry( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_2.id assert result["result"].data == { CONF_HOST: DUMMY_HOST2, @@ -146,5 +146,5 @@ async def test_already_configured(hass: HomeAssistant, mock_hub_discover) -> Non DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index b2342b7c2a7..579ca2019f8 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with ( patch( @@ -58,7 +58,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == str(TEST_DATA["account_id"]) assert result3["data"] == { ACCOUNT_ID: TEST_DATA["account_id"], @@ -80,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "adax.get_adax_token", @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], TEST_DATA, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -115,7 +115,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("adax.get_adax_token", return_value="token"): result3 = await hass.config_entries.flow.async_configure( @@ -124,7 +124,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -136,7 +136,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -145,7 +145,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -173,7 +173,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: ) test_data[CONNECTION_TYPE] = LOCAL - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "8383838" assert result["data"] == { "connection_type": "Local", @@ -201,7 +201,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -210,7 +210,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -229,7 +229,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -239,7 +239,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -248,7 +248,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -264,7 +264,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -274,7 +274,7 @@ async def test_local_heater_not_available(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -283,7 +283,7 @@ async def test_local_heater_not_available(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -299,7 +299,7 @@ async def test_local_heater_not_available(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "heater_not_available" @@ -309,7 +309,7 @@ async def test_local_heater_not_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -318,7 +318,7 @@ async def test_local_heater_not_found(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -334,7 +334,7 @@ async def test_local_heater_not_found(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "heater_not_found" @@ -344,7 +344,7 @@ async def test_local_invalid_wifi_cred(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -353,7 +353,7 @@ async def test_local_invalid_wifi_cred(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -369,5 +369,5 @@ async def test_local_invalid_wifi_cred(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 3f12dd1508a..d493962611f 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -2,7 +2,7 @@ import aiohttp -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.adguard.const import DOMAIN from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import SOURCE_USER @@ -16,6 +16,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -36,7 +37,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -58,7 +59,7 @@ async def test_connection_error( ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -83,14 +84,14 @@ async def test_full_flow_implementation( assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=FIXTURE_USER_INPUT ) assert result2 - assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] data = result2.get("data") @@ -115,7 +116,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, ) assert result - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -140,7 +141,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -165,7 +166,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -194,14 +195,14 @@ async def test_hassio_confirm( context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2 - assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "AdGuard Home Addon" data = result2.get("data") @@ -240,6 +241,6 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 4395fb82542..2eb95c18b7d 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -76,8 +76,8 @@ async def test_binary_sensor_async_setup_entry( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() - assert len(mock_get.mock_calls) == 2 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state @@ -100,7 +100,7 @@ async def test_binary_sensor_async_setup_entry( 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) assert len(mock_get.mock_calls) == 2 state = hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 134cfee9f68..d0f200a9ca5 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import AsyncMock, patch from advantage_air import ApiError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.advantage_air.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_SYSTEM_DATA, USER_INPUT @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user" assert result1["errors"] == {} @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: mock_setup_entry.assert_called_once() mock_get.assert_called_once() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "testname" assert result2["data"] == USER_INPUT @@ -55,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: result3["flow_id"], USER_INPUT, ) - assert result4["type"] == data_entry_flow.FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -74,6 +75,6 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) mock_get.assert_called_once() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 967afe20ddb..ced1ff3a9e7 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -125,14 +125,14 @@ async def test_sensor_platform_disabled_entity( mock_get.reset_mock() entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) - 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=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() - assert len(mock_get.mock_calls) == 2 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index a7a689381e0..45fec473396 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -6,11 +6,11 @@ from aemet_opendata.exceptions import AuthError from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant import data_entry_flow from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import mock_api_call @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -51,7 +51,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -89,14 +89,14 @@ async def test_form_options( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: expected, } @@ -127,7 +127,7 @@ async def test_form_duplicated_id( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/aftership/test_config_flow.py b/tests/components/aftership/test_config_flow.py index d5e34ac0ae2..34a23e31918 100644 --- a/tests/components/aftership/test_config_flow.py +++ b/tests/components/aftership/test_config_flow.py @@ -29,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: CONF_API_KEY: "mock-api-key", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "AfterShip" assert result["data"] == { CONF_API_KEY: "mock-api-key", @@ -54,7 +54,7 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non CONF_API_KEY: "mock-api-key", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -68,7 +68,7 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non CONF_API_KEY: "mock-api-key", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "AfterShip" assert result["data"] == { CONF_API_KEY: "mock-api-key", diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 958ec97a3ca..fee8a40f4f7 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -2,12 +2,12 @@ import pytest -from homeassistant import data_entry_flow from homeassistant.components.agent_dvr import config_flow from homeassistant.components.agent_dvr.const import SERVER_URL from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -25,7 +25,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_user_device_exists_abort( @@ -40,7 +40,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "example.local", CONF_PORT: 8090}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_connection_error( @@ -58,7 +58,7 @@ async def test_connection_error( assert result["errors"]["base"] == "cannot_connect" assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_full_user_flow_implementation( @@ -83,7 +83,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090} @@ -93,7 +93,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 8090 assert result["data"][SERVER_URL] == "http://example.local:8090/" assert result["title"] == "DESKTOP" - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447" diff --git a/tests/components/agent_dvr/test_init.py b/tests/components/agent_dvr/test_init.py index 5b360430b78..7f546a190a7 100644 --- a/tests/components/agent_dvr/test_init.py +++ b/tests/components/agent_dvr/test_init.py @@ -31,7 +31,7 @@ async def test_setup_config_and_unload( ) -> None: """Test setup and unload.""" entry = await init_integration(hass, aioclient_mock) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -50,7 +50,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: side_effect=AgentError, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with _patch_init_agent(await _create_mocked_agent(available=False)): await hass.config_entries.async_reload(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index fcc024f7cee..7c0cac805d3 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -4,11 +4,11 @@ from http import HTTPStatus from airly.exceptions import AirlyError -from homeassistant import data_entry_flow from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import API_NEAREST_URL, API_POINT_URL @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -83,7 +83,7 @@ async def test_invalid_location_for_point_and_nearest( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_location" @@ -98,7 +98,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -113,7 +113,7 @@ async def test_create_entry( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -137,7 +137,7 @@ async def test_create_entry_with_nearest_method( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index ece28a77a87..b62cb43844b 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.airnow.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,11 +19,11 @@ async def test_form(hass: HomeAssistant, config, options, setup_airnow) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == config assert result2["options"] == options @@ -34,7 +35,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, config, setup_airnow) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -45,7 +46,7 @@ async def test_form_invalid_location(hass: HomeAssistant, config, setup_airnow) DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_location"} @@ -56,7 +57,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, config, setup_airnow) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -67,7 +68,7 @@ async def test_form_empty_result(hass: HomeAssistant, config, setup_airnow) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_location"} @@ -78,7 +79,7 @@ async def test_form_unexpected(hass: HomeAssistant, config, setup_airnow) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -88,7 +89,7 @@ async def test_entry_already_exists(hass: HomeAssistant, config, config_entry) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -140,7 +141,7 @@ async def test_options_flow(hass: HomeAssistant, setup_airnow) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -153,7 +154,7 @@ async def test_options_flow(hass: HomeAssistant, setup_airnow) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_RADIUS: 25, } diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py index 9c5492eaa20..8c85e017367 100644 --- a/tests/components/airq/test_config_flow.py +++ b/tests/components/airq/test_config_flow.py @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -47,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DEVICE_INFO["name"] assert result2["data"] == TEST_USER_DATA @@ -63,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], TEST_USER_DATA | {CONF_PASSWORD: "wrong_password"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -78,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], TEST_USER_DATA ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,5 +101,5 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_USER_DATA ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 2ea157f09b1..081e1bfd86d 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -43,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Airthings" assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -83,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -102,7 +102,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -121,5 +121,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TEST_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index d60b42eddf2..3622a21f633 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -12,7 +12,8 @@ from airthings_ble import ( from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry from tests.common import MockConfigEntry, MockEntity from tests.components.bluetooth import generate_advertisement_data, generate_ble_device @@ -232,14 +233,12 @@ def create_entry(hass): return entry -def create_device(hass, entry): +def create_device(entry: ConfigEntry, device_registry: DeviceRegistry): """Create a device for the given entry.""" - device_registry = hass.helpers.device_registry.async_get(hass) - device = device_registry.async_get_or_create( + return device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)}, manufacturer="Airthings AS", name="Airthings Wave Plus (123456)", model="Wave Plus", ) - return device diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index edeb08abb74..f6a7098785b 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -43,7 +43,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: data=WAVE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == { "name": "Airthings Wave Plus (123456)" @@ -54,7 +54,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: result["flow_id"], user_input={"not": "empty"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Airthings Wave Plus (123456)" assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" @@ -67,7 +67,7 @@ async def test_bluetooth_discovery_no_BLEDevice(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WAVE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -87,7 +87,7 @@ async def test_bluetooth_discovery_airthings_ble_update_failed( data=WAVE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -103,7 +103,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WAVE_DEVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -146,7 +146,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Airthings Wave Plus (123456)" assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" @@ -160,7 +160,7 @@ async def test_user_setup_no_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -178,7 +178,7 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -196,7 +196,7 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -214,7 +214,7 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -227,5 +227,5 @@ async def test_unsupported_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index eee4de263d6..9949528ccc7 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -5,6 +5,7 @@ import logging from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.airthings_ble import ( CO2_V1, @@ -25,18 +26,20 @@ from tests.components.bluetooth import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) -async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): +async def test_migration_from_v1_to_v3_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format.""" entry = create_entry(hass) - device = create_device(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None new_unique_id = f"{WAVE_DEVICE_INFO.address}_temperature" - entity_registry = hass.helpers.entity_registry.async_get(hass) - sensor = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, @@ -64,18 +67,18 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id -async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): +async def test_migration_from_v2_to_v3_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """Verify that we can migrate from v2 (introduced in 2023.9.0) to the latest unique id format.""" entry = create_entry(hass) - device = create_device(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None - entity_registry = hass.helpers.entity_registry.async_get(hass) - - await hass.async_block_till_done() - sensor = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, @@ -105,18 +108,18 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id -async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): +async def test_migration_from_v1_and_v2_to_v3_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """Test if migration works when we have both v1 (pre 2023.9.0) and v2 (introduced in 2023.9.0) unique ids.""" entry = create_entry(hass) - device = create_device(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None - entity_registry = hass.helpers.entity_registry.async_get(hass) - - await hass.async_block_till_done() - v2 = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, @@ -155,18 +158,18 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): assert entity_registry.async_get(v2.entity_id).unique_id == CO2_V2.unique_id -async def test_migration_with_all_unique_ids(hass: HomeAssistant): +async def test_migration_with_all_unique_ids( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """Test if migration works when we have all unique ids.""" entry = create_entry(hass) - device = create_device(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None - entity_registry = hass.helpers.entity_registry.async_get(hass) - - await hass.async_block_till_done() - v1 = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, diff --git a/tests/components/airtouch4/test_config_flow.py b/tests/components/airtouch4/test_config_flow.py index e5e3672f69d..d574531faa7 100644 --- a/tests/components/airtouch4/test_config_flow.py +++ b/tests/components/airtouch4/test_config_flow.py @@ -7,6 +7,7 @@ from airtouch4pyapi.airtouch import AirTouch, AirTouchAc, AirTouchGroup, AirTouc from homeassistant import config_entries from homeassistant.components.airtouch4.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -14,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_ac = AirTouchAc() mock_groups = AirTouchGroup() @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "0.0.0.1" assert result2["data"] == { "host": "0.0.0.1", @@ -62,7 +63,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -81,7 +82,7 @@ async def test_form_library_error_message(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -100,7 +101,7 @@ async def test_form_connection_refused(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -124,5 +125,5 @@ async def test_form_no_units(hass: HomeAssistant) -> None: result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_units"} diff --git a/tests/components/airtouch5/test_config_flow.py b/tests/components/airtouch5/test_config_flow.py index 8b4b9890e57..9a294d5a4f5 100644 --- a/tests/components/airtouch5/test_config_flow.py +++ b/tests/components/airtouch5/test_config_flow.py @@ -17,7 +17,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None host = "1.1.1.1" @@ -34,7 +34,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == host assert result2["data"] == { "host": host, @@ -59,5 +59,5 @@ async def test_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 8c5bbded662..b9643b17c07 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -11,7 +11,6 @@ from pyairvisual.cloud_api import ( from pyairvisual.errors import AirVisualError import pytest -from homeassistant import data_entry_flow from homeassistant.components.airvisual import ( CONF_CITY, CONF_INTEGRATION_TYPE, @@ -22,6 +21,7 @@ from homeassistant.components.airvisual import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( COORDS_CONFIG, @@ -81,13 +81,13 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"type": integration_type} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == input_form_step # Test errors that can arise: @@ -95,14 +95,14 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors # Test that we can recover and finish the flow after errors occur: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_title assert result["data"] == {**config, CONF_INTEGRATION_TYPE: integration_type} @@ -112,7 +112,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -120,13 +120,13 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) context={"source": SOURCE_USER}, data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "geography_by_coords" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,13 +135,13 @@ async def test_options_flow( ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -152,11 +152,11 @@ async def test_step_reauth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config_entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_api_key = "defgh67890" @@ -164,7 +164,7 @@ async def test_step_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/airvisual_pro/test_config_flow.py b/tests/components/airvisual_pro/test_config_flow.py index b0469b5288b..803a335f52c 100644 --- a/tests/components/airvisual_pro/test_config_flow.py +++ b/tests/components/airvisual_pro/test_config_flow.py @@ -9,11 +9,11 @@ from pyairvisual.node import ( ) import pytest -from homeassistant import data_entry_flow from homeassistant.components.airvisual_pro.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -34,7 +34,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when connecting to a Pro: @@ -42,13 +42,13 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == connect_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.101" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.101", @@ -63,13 +63,13 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +78,7 @@ async def test_step_import(hass: HomeAssistant, config, setup_airvisual_pro) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.101" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.101", @@ -114,7 +114,7 @@ async def test_reauth( }, data=config, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Test errors that can arise when connecting to a Pro: @@ -122,7 +122,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "new_password"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == connect_errors result = await hass.config_entries.flow.async_configure( @@ -132,6 +132,6 @@ async def test_reauth( # Allow reload to finish: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index c47e2b1a3dd..072699c7a26 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -76,7 +76,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -90,7 +90,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] @@ -132,7 +132,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -143,7 +143,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: result["flow_id"], CONFIG_ID1 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -151,7 +151,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" @@ -177,7 +177,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -208,7 +208,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with ( @@ -244,7 +244,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: TEST_IP, CONF_PORT: TEST_PORT, @@ -266,7 +266,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -283,7 +283,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -338,7 +338,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {short_mac(HVAC_WEBSERVER_MOCK[API_MAC])}" assert result["data"][CONF_HOST] == TEST_IP assert result["data"][CONF_PORT] == TEST_PORT @@ -359,7 +359,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with ( @@ -395,7 +395,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -416,7 +416,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {short_mac(DHCP_SERVICE_INFO.macaddress)}" assert result["data"][CONF_HOST] == TEST_IP assert result["data"][CONF_PORT] == TEST_PORT diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index d1a8d74cc08..3309c175543 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -109,6 +109,7 @@ 'action': 6, 'active': False, 'available': True, + 'double-set-point': False, 'id': 'aidoo1', 'installation': 'installation1', 'is-connected': True, @@ -150,6 +151,7 @@ 'action': 1, 'active': True, 'available': True, + 'double-set-point': True, 'id': 'aidoo_pro', 'installation': 'installation1', 'is-connected': True, @@ -177,7 +179,7 @@ 'temperature': 20.0, 'temperature-setpoint': 22.0, 'temperature-setpoint-cool-air': 22.0, - 'temperature-setpoint-hot-air': 22.0, + 'temperature-setpoint-hot-air': 18.0, 'temperature-setpoint-max': 30.0, 'temperature-setpoint-max-auto-air': 30.0, 'temperature-setpoint-max-cool-air': 30.0, @@ -196,6 +198,9 @@ 'action': 1, 'active': True, 'available': True, + 'hot-water': list([ + 'dhw1', + ]), 'humidity': 27, 'id': 'group1', 'installation': 'installation1', @@ -275,6 +280,32 @@ 'temperature-step': 0.5, }), }), + 'hot-water': dict({ + 'dhw1': dict({ + 'active': False, + 'available': True, + 'id': 'dhw1', + 'installation': 'installation1', + 'is-connected': True, + 'name': 'Airzone Cloud DHW', + 'operation': 0, + 'operations': list([ + 0, + 1, + 2, + ]), + 'power': False, + 'power-mode': False, + 'problems': False, + 'temperature': 45.5, + 'temperature-setpoint': 48, + 'temperature-setpoint-max': 60, + 'temperature-setpoint-min': 40, + 'temperature-step': 1, + 'web-server': 'webserver1', + 'ws-connected': True, + }), + }), 'installations': dict({ 'installation1': dict({ 'action': 1, @@ -289,6 +320,9 @@ 'group2', 'group3', ]), + 'hot-water': list([ + 'dhw1', + ]), 'humidity': 27, 'id': 'installation1', 'mode': 2, @@ -418,6 +452,7 @@ 'aq-present': True, 'aq-status': 'good', 'available': True, + 'double-set-point': False, 'humidity': 30, 'id': 'zone1', 'installation': 'installation1', @@ -478,6 +513,7 @@ 'aq-present': True, 'aq-status': 'good', 'available': True, + 'double-set-point': False, 'humidity': 24, 'id': 'zone2', 'installation': 'installation1', diff --git a/tests/components/airzone_cloud/test_config_flow.py b/tests/components/airzone_cloud/test_config_flow.py index e1d31e28d4b..86a70ced51a 100644 --- a/tests/components/airzone_cloud/test_config_flow.py +++ b/tests/components/airzone_cloud/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch from aioairzone_cloud.exceptions import AirzoneCloudError, LoginError -from homeassistant import data_entry_flow from homeassistant.components.airzone_cloud.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import ( CONFIG, @@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -65,7 +65,7 @@ async def test_form(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -82,7 +82,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"House {WS_ID} ({CONFIG[CONF_ID]})" assert result["data"][CONF_ID] == CONFIG[CONF_ID] assert result["data"][CONF_USERNAME] == CONFIG[CONF_USERNAME] @@ -120,7 +120,7 @@ async def test_installations_list_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -132,7 +132,7 @@ async def test_installations_list_error(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/airzone_cloud/test_select.py b/tests/components/airzone_cloud/test_select.py new file mode 100644 index 00000000000..1375b052050 --- /dev/null +++ b/tests/components/airzone_cloud/test_select.py @@ -0,0 +1,61 @@ +"""The select tests for the Airzone Cloud platform.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from .util import async_init_integration + + +async def test_airzone_create_selects( + hass: HomeAssistant, entity_registry_enabled_by_default: None +) -> None: + """Test creation of selects.""" + + await async_init_integration(hass) + + # Zones + state = hass.states.get("select.dormitorio_air_quality_mode") + assert state.state == "auto" + + state = hass.states.get("select.salon_air_quality_mode") + assert state.state == "auto" + + +async def test_airzone_select_air_quality_mode(hass: HomeAssistant) -> None: + """Test select Air Quality mode.""" + + await async_init_integration(hass) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dormitorio_air_quality_mode", + ATTR_OPTION: "Invalid", + }, + blocking=True, + ) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dormitorio_air_quality_mode", + ATTR_OPTION: "off", + }, + blocking=True, + ) + + state = hass.states.get("select.dormitorio_air_quality_mode") + assert state.state == "off" diff --git a/tests/components/airzone_cloud/test_water_heater.py b/tests/components/airzone_cloud/test_water_heater.py new file mode 100644 index 00000000000..98b1d85be48 --- /dev/null +++ b/tests/components/airzone_cloud/test_water_heater.py @@ -0,0 +1,186 @@ +"""The water heater tests for the Airzone Cloud platform.""" + +from unittest.mock import patch + +from aioairzone_cloud.exceptions import AirzoneCloudError +import pytest + +from homeassistant.components.water_heater import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_OPERATION_MODE, + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_PERFORMANCE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + + +async def test_airzone_create_water_heater(hass: HomeAssistant) -> None: + """Test creation of water heater.""" + + await async_init_integration(hass) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 45.5 + assert state.attributes[ATTR_MAX_TEMP] == 60 + assert state.attributes[ATTR_MIN_TEMP] == 40 + assert state.attributes[ATTR_TEMPERATURE] == 48 + + +async def test_airzone_water_heater_turn_on_off(hass: HomeAssistant) -> None: + """Test turning on/off.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_ECO + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_OFF + + +async def test_airzone_water_heater_set_operation(hass: HomeAssistant) -> None: + """Test setting the Operation mode.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_OPERATION_MODE: STATE_ECO, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_ECO + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_OPERATION_MODE: STATE_PERFORMANCE, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_PERFORMANCE + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_OPERATION_MODE: STATE_OFF, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_OFF + + +async def test_airzone_water_heater_set_temp(hass: HomeAssistant) -> None: + """Test setting the target temperature.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_TEMPERATURE: 50, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.attributes[ATTR_TEMPERATURE] == 50 + + +async def test_airzone_water_heater_set_temp_error(hass: HomeAssistant) -> None: + """Test error when setting the target temperature.""" + + await async_init_integration(hass) + + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + side_effect=AirzoneCloudError, + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_TEMPERATURE: 80, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.attributes[ATTR_TEMPERATURE] == 48 diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index ea0dbf9f736..0583fad7c0e 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -14,6 +14,7 @@ from aioairzone_cloud.const import ( API_AQ_PM_10, API_AQ_PRESENT, API_AQ_QUALITY, + API_AZ_ACS, API_AZ_AIDOO, API_AZ_AIDOO_PRO, API_AZ_SYSTEM, @@ -24,6 +25,7 @@ from aioairzone_cloud.const import ( API_DEVICE_ID, API_DEVICES, API_DISCONNECTION_DATE, + API_DOUBLE_SET_POINT, API_ERRORS, API_FAH, API_GROUP_ID, @@ -39,8 +41,10 @@ from aioairzone_cloud.const import ( API_NAME, API_OLD_ID, API_POWER, + API_POWERFUL_MODE, API_RANGE_MAX_AIR, API_RANGE_MIN_AIR, + API_RANGE_SP_MAX_ACS, API_RANGE_SP_MAX_AUTO_AIR, API_RANGE_SP_MAX_COOL_AIR, API_RANGE_SP_MAX_DRY_AIR, @@ -48,6 +52,7 @@ from aioairzone_cloud.const import ( API_RANGE_SP_MAX_HOT_AIR, API_RANGE_SP_MAX_STOP_AIR, API_RANGE_SP_MAX_VENT_AIR, + API_RANGE_SP_MIN_ACS, API_RANGE_SP_MIN_AUTO_AIR, API_RANGE_SP_MIN_COOL_AIR, API_RANGE_SP_MIN_DRY_AIR, @@ -55,6 +60,7 @@ from aioairzone_cloud.const import ( API_RANGE_SP_MIN_HOT_AIR, API_RANGE_SP_MIN_STOP_AIR, API_RANGE_SP_MIN_VENT_AIR, + API_SETPOINT, API_SP_AIR_AUTO, API_SP_AIR_COOL, API_SP_AIR_DRY, @@ -70,7 +76,9 @@ from aioairzone_cloud.const import ( API_STAT_RSSI, API_STAT_SSID, API_STATUS, + API_STEP, API_SYSTEM_NUMBER, + API_TANK_TEMP, API_TYPE, API_WARNINGS, API_WS_CONNECTED, @@ -105,6 +113,11 @@ GET_INSTALLATION_MOCK = { API_GROUP_ID: "grp1", API_NAME: "Group", API_DEVICES: [ + { + API_DEVICE_ID: "dhw1", + API_TYPE: API_AZ_ACS, + API_WS_ID: WS_ID, + }, { API_DEVICE_ID: "system1", API_TYPE: API_AZ_SYSTEM, @@ -268,6 +281,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: if device.get_id() == "aidoo_pro": return { API_ACTIVE: True, + API_DOUBLE_SET_POINT: True, API_ERRORS: [], API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [ @@ -279,7 +293,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: ], API_SP_AIR_AUTO: {API_CELSIUS: 22, API_FAH: 72}, API_SP_AIR_COOL: {API_CELSIUS: 22, API_FAH: 72}, - API_SP_AIR_HEAT: {API_CELSIUS: 22, API_FAH: 72}, + API_SP_AIR_HEAT: {API_CELSIUS: 18, API_FAH: 64}, API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, API_RANGE_SP_MAX_AUTO_AIR: {API_CELSIUS: 30, API_FAH: 86}, API_RANGE_SP_MAX_COOL_AIR: {API_CELSIUS: 30, API_FAH: 86}, @@ -297,6 +311,21 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_LOCAL_TEMP: {API_CELSIUS: 20, API_FAH: 68}, API_WARNINGS: [], } + if device.get_id() == "dhw1": + return { + API_ACTIVE: False, + API_ERRORS: [], + API_POWER: False, + API_POWERFUL_MODE: False, + API_SETPOINT: {API_CELSIUS: 48, API_FAH: 118}, + API_RANGE_SP_MAX_ACS: {API_CELSIUS: 60, API_FAH: 140}, + API_RANGE_SP_MIN_ACS: {API_CELSIUS: 40, API_FAH: 104}, + API_STEP: {API_CELSIUS: 1, API_FAH: 1}, + API_TANK_TEMP: {API_CELSIUS: 45.5, API_FAH: 114}, + API_IS_CONNECTED: True, + API_WS_CONNECTED: True, + API_WARNINGS: [], + } if device.get_id() == "system1": return { API_AQ_MODE_VALUES: ["off", "on", "auto"], @@ -332,6 +361,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_AQ_PM_10: 3, API_AQ_PRESENT: True, API_AQ_QUALITY: "good", + API_DOUBLE_SET_POINT: False, API_HUMIDITY: 30, API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [ @@ -376,6 +406,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_AQ_PM_10: 3, API_AQ_PRESENT: True, API_AQ_QUALITY: "good", + API_DOUBLE_SET_POINT: False, API_HUMIDITY: 24, API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [], diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 90cf269b3f8..65b8b24a59d 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -73,7 +73,7 @@ async def test_form_failed_auth( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +97,7 @@ async def test_form_connection_timeout( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -116,7 +116,7 @@ async def test_form_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -132,7 +132,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -159,7 +159,7 @@ async def test_reauth_flow( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -178,7 +178,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -209,7 +209,7 @@ async def test_reauth_flow_auth_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False mock_aladdinconnect_api.login.side_effect = InvalidPasswordError @@ -233,7 +233,7 @@ async def test_reauth_flow_auth_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -260,7 +260,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.side_effect = ClientConnectionError @@ -274,5 +274,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 9ad9febc762..082ade75ab9 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -114,7 +114,7 @@ async def test_cover_operation( 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 is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert COVER_DOMAIN in hass.config.components diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index c995fb5074d..623c121957b 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -93,7 +93,7 @@ async def test_setup_component_no_error(hass: HomeAssistant) -> None: assert 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 is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -135,12 +135,12 @@ async def test_load_and_unload( 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 is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_stale_device_removal( @@ -190,7 +190,7 @@ async def test_stale_device_removal( assert 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 is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 device_registry = dr.async_get(hass) @@ -220,7 +220,7 @@ async def test_stale_device_removal( assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED mock_aladdinconnect_api.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) with patch( @@ -230,7 +230,7 @@ async def test_stale_device_removal( assert 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 is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 device_entries = dr.async_entries_for_config_entry( diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index afcfa0a7a12..5d142ab277b 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -3,11 +3,11 @@ import pytest from pytest_unordered import unordered +from homeassistant.components import automation from homeassistant.components.alarm_control_panel import ( DOMAIN, AlarmControlPanelEntityFeature, ) -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index d95574b7c9f..b6ee6b2faaa 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -3,11 +3,11 @@ import pytest from pytest_unordered import unordered +from homeassistant.components import automation from homeassistant.components.alarm_control_panel import ( DOMAIN, AlarmControlPanelEntityFeature, ) -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index be241ef241e..fb2d4e0a504 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -5,11 +5,11 @@ from datetime import timedelta import pytest from pytest_unordered import unordered +from homeassistant.components import automation from homeassistant.components.alarm_control_panel import ( DOMAIN, AlarmControlPanelEntityFeature, ) -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -497,15 +497,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -564,15 +561,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 614d055405e..bd71795b4c9 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from alarmdecoder.util import NoDeviceError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.alarmdecoder import config_flow from homeassistant.components.alarmdecoder.const import ( CONF_ALT_NIGHT_MODE, @@ -31,6 +31,7 @@ from homeassistant.components.alarmdecoder.const import ( from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -63,7 +64,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -71,7 +72,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol" with ( @@ -85,7 +86,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == { **connection, @@ -108,7 +109,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -116,7 +117,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol" with ( @@ -129,7 +130,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with ( @@ -142,7 +143,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -161,7 +162,7 @@ async def test_options_arm_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -169,7 +170,7 @@ async def test_options_arm_flow(hass: HomeAssistant) -> None: user_input={"edit_selection": "Arming Settings"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "arm_settings" with patch( @@ -180,7 +181,7 @@ async def test_options_arm_flow(hass: HomeAssistant) -> None: user_input=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: user_input, OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS, @@ -202,7 +203,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -210,7 +211,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -226,7 +227,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input=zone_settings, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {zone_number: zone_settings}, @@ -235,7 +236,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: # Make sure zone can be removed... result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,7 +244,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -259,7 +260,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {}, @@ -281,7 +282,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -289,7 +290,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" # Zone Number must be int @@ -298,7 +299,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={CONF_ZONE_NUMBER: "asd"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" assert result["errors"] == {CONF_ZONE_NUMBER: "int"} @@ -307,7 +308,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={CONF_ZONE_NUMBER: zone_number}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive @@ -316,7 +317,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_RELAY_ADDR: "1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -325,7 +326,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_RELAY_CHAN: "1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -335,7 +336,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == { CONF_RELAY_ADDR: "int", @@ -348,7 +349,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_ZONE_LOOP: "1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"} @@ -358,7 +359,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "int"} @@ -368,7 +369,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"} @@ -387,7 +388,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: { @@ -435,7 +436,7 @@ async def test_one_device_allowed(hass: HomeAssistant, protocol, connection) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -443,11 +444,11 @@ async def test_one_device_allowed(hass: HomeAssistant, protocol, connection) -> {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol" result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index 7c4030b56da..31236c84f34 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -4,7 +4,7 @@ from copy import deepcopy import pytest -import homeassistant.components.alert as alert +from homeassistant.components import alert, notify from homeassistant.components.alert.const import ( CONF_ALERT_MESSAGE, CONF_DATA, @@ -14,7 +14,6 @@ from homeassistant.components.alert.const import ( CONF_TITLE, DOMAIN, ) -import homeassistant.components.notify as notify from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 0cc4d995efa..9fdcc1c89c1 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -158,14 +158,14 @@ async def assert_power_controller_works( _, response = await assert_request_calls_service( "Alexa.PowerController", "TurnOn", endpoint, on_service, hass ) - for property in response["context"]["properties"]: - assert property["timeOfSample"] == timestamp + for context_property in response["context"]["properties"]: + assert context_property["timeOfSample"] == timestamp _, response = await assert_request_calls_service( "Alexa.PowerController", "TurnOff", endpoint, off_service, hass ) - for property in response["context"]["properties"]: - assert property["timeOfSample"] == timestamp + for context_property in response["context"]["properties"]: + assert context_property["timeOfSample"] == timestamp async def assert_scene_controller_works( diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index b7e6a5e53ac..fa8d7a2c9fb 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components import camera from homeassistant.components.alexa import smart_home, state_report -import homeassistant.components.camera as camera from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 153442552a4..1c30c72e72c 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -23,12 +23,11 @@ async def do_http_discovery(config, hass, hass_client): http_client = await hass_client() request = get_new_request("Alexa.Discovery", "Discover") - response = await http_client.post( + return await http_client.post( smart_home.SMART_HOME_HTTP_ENDPOINT, data=json.dumps(request), headers={"content-type": CONTENT_TYPE_JSON}, ) - return response @pytest.mark.parametrize( diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 6bd7caccc38..92410ae9de9 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -185,14 +185,14 @@ async def test_report_state_unsets_authorized_on_error( config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) + config._store.set_authorized.assert_not_called() + hass.states.async_set( "binary_sensor.test_contact", "off", {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - config._store.set_authorized.assert_not_called() - # To trigger event listener await hass.async_block_till_done() config._store.set_authorized.assert_called_once_with(False) @@ -215,15 +215,15 @@ async def test_report_state_unsets_authorized_on_access_token_error( await state_report.async_enable_proactive_mode(hass, config) - hass.states.async_set( - "binary_sensor.test_contact", - "off", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) - config._store.set_authorized.assert_not_called() with patch.object(config, "async_get_access_token", AsyncMock(side_effect=exc)): + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + # To trigger event listener await hass.async_block_till_done() config._store.set_authorized.assert_called_once_with(False) @@ -731,39 +731,39 @@ async def test_proactive_mode_filter_states( assert len(aioclient_mock.mock_calls) == 0 # hass not running should not report + current_state = hass.state + hass.set_state(core.CoreState.stopping) + await hass.async_block_till_done() + await hass.async_block_till_done() hass.states.async_set( "binary_sensor.test_contact", "off", {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - current_state = hass.state - hass.set_state(core.CoreState.stopping) - await hass.async_block_till_done() - await hass.async_block_till_done() hass.set_state(current_state) assert len(aioclient_mock.mock_calls) == 0 # unsupported entity should not report - hass.states.async_set( - "binary_sensor.test_contact", - "on", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) with patch.dict( "homeassistant.components.alexa.state_report.ENTITY_ADAPTERS", {}, clear=True ): + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) await hass.async_block_till_done() await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 # Not exposed by config should not report - hass.states.async_set( - "binary_sensor.test_contact", - "off", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) with patch.object(config, "should_expose", return_value=False): + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) await hass.async_block_till_done() await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index 2624bd96d31..030b82d3596 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -8,7 +8,6 @@ from amberelectric import ApiException from amberelectric.model.site import Site, SiteStatus import pytest -from homeassistant import data_entry_flow from homeassistant.components.amberelectric.config_flow import filter_sites from homeassistant.components.amberelectric.const import ( CONF_SITE_ID, @@ -18,6 +17,7 @@ from homeassistant.components.amberelectric.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType API_KEY = "psk_123456789" @@ -131,7 +131,7 @@ async def test_single_pending_site( initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("type") is FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -140,7 +140,7 @@ async def test_single_pending_site( context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("type") is FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -149,7 +149,7 @@ async def test_single_pending_site( ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -162,7 +162,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("type") is FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -171,7 +171,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("type") is FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -180,7 +180,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -195,7 +195,7 @@ async def test_single_site_rejoin( initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("type") is FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -204,7 +204,7 @@ async def test_single_site_rejoin( context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("type") is FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -213,7 +213,7 @@ async def test_single_site_rejoin( ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -229,7 +229,7 @@ async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None: data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "no_site"} @@ -240,7 +240,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -249,7 +249,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "invalid_api_token"} @@ -260,7 +260,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -269,7 +269,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "unknown_error"} diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index e9b85eaaa40..67c67aba4a8 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import AsyncMock, patch import ambiclimate import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries 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 +from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp @@ -38,7 +39,7 @@ async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> Non flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -51,12 +52,12 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - with pytest.raises(data_entry_flow.AbortFlow): + with pytest.raises(AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +67,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert ( result["description_placeholders"]["cb_url"] @@ -81,7 +82,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Ambiclimate" assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate" assert result["data"][CONF_CLIENT_SECRET] == "secret" @@ -89,14 +90,14 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT with patch( "ambiclimate.AmbiclimateOAuth.get_access_token", side_effect=ambiclimate.AmbiclimateOauthError(), ): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_abort_invalid_code(hass: HomeAssistant) -> None: @@ -106,7 +107,7 @@ async def test_abort_invalid_code(hass: HomeAssistant) -> None: with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "access_token" @@ -118,7 +119,7 @@ async def test_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_network/__init__.py b/tests/components/ambient_network/__init__.py new file mode 100644 index 00000000000..2971b77ddd8 --- /dev/null +++ b/tests/components/ambient_network/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ambient Weather Network integration.""" diff --git a/tests/components/ambient_network/conftest.py b/tests/components/ambient_network/conftest.py new file mode 100644 index 00000000000..ede44b5d92f --- /dev/null +++ b/tests/components/ambient_network/conftest.py @@ -0,0 +1,89 @@ +"""Common fixtures for the Ambient Weather Network integration tests.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, Mock, patch + +from aioambient import OpenAPI +import pytest + +from homeassistant.components import ambient_network +from homeassistant.core import HomeAssistant + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.ambient_network.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="devices_by_location", scope="package") +def devices_by_location_fixture() -> list[dict[str, Any]]: + """Return result of OpenAPI get_devices_by_location() call.""" + return load_json_array_fixture( + "devices_by_location_response.json", "ambient_network" + ) + + +def mock_device_details_callable(mac_address: str) -> dict[str, Any]: + """Return result of OpenAPI get_device_details() call.""" + return load_json_object_fixture( + f"device_details_response_{mac_address[0].lower()}.json", "ambient_network" + ) + + +@pytest.fixture(name="open_api") +def mock_open_api() -> OpenAPI: + """Mock OpenAPI object.""" + return Mock( + get_device_details=AsyncMock(side_effect=mock_device_details_callable), + ) + + +@pytest.fixture(name="aioambient") +async def mock_aioambient(open_api: OpenAPI): + """Mock aioambient library.""" + with ( + patch( + "homeassistant.components.ambient_network.config_flow.OpenAPI", + return_value=open_api, + ), + patch( + "homeassistant.components.ambient_network.OpenAPI", + return_value=open_api, + ), + ): + yield + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(request) -> MockConfigEntry: + """Mock config entry.""" + return MockConfigEntry( + domain=ambient_network.DOMAIN, + title=f"Station {request.param[0]}", + data={"mac": request.param}, + ) + + +async def setup_platform( + expected_outcome: bool, + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Load the Ambient Network integration with the provided OpenAPI and config entry.""" + + config_entry.add_to_hass(hass) + assert ( + await hass.config_entries.async_setup(config_entry.entry_id) == expected_outcome + ) + await hass.async_block_till_done() diff --git a/tests/components/ambient_network/fixtures/device_details_response_a.json b/tests/components/ambient_network/fixtures/device_details_response_a.json new file mode 100644 index 00000000000..40491e2631c --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_a.json @@ -0,0 +1,34 @@ +{ + "_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "macAddress": "AA:AA:AA:AA:AA:AA", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "dateutc": 1699474320000, + "tempf": 82.9, + "dewPoint": 82.0, + "feelsLike": 85.0, + "humidity": 60, + "windspeedmph": 8.72, + "windgustmph": 9.17, + "maxdailygust": 22.82, + "winddir": 11, + "uv": 0, + "solarradiation": 37.64, + "hourlyrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0, + "monthlyrainin": 0, + "totalrainin": 26.402, + "baromrelin": 29.586, + "baromabsin": 28.869, + "batt_co2": 1, + "type": "weather-data", + "created_at": 1699474320914, + "dateutc5": 1699474200000, + "lastRain": 1698659100000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station A" + } +} diff --git a/tests/components/ambient_network/fixtures/device_details_response_b.json b/tests/components/ambient_network/fixtures/device_details_response_b.json new file mode 100644 index 00000000000..8249f6f0c30 --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_b.json @@ -0,0 +1,7 @@ +{ + "_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "macAddress": "BB:BB:BB:BB:BB:BB", + "info": { + "name": "Station B" + } +} diff --git a/tests/components/ambient_network/fixtures/device_details_response_c.json b/tests/components/ambient_network/fixtures/device_details_response_c.json new file mode 100644 index 00000000000..8e171f35374 --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_c.json @@ -0,0 +1,33 @@ +{ + "_id": "cccccccccccccccccccccccccccccccc", + "macAddress": "CC:CC:CC:CC:CC:CC", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "dateutc": 1699474320000, + "tempf": 82.9, + "dewPoint": 82.0, + "feelsLike": 85.0, + "humidity": 60, + "windspeedmph": 8.72, + "windgustmph": 9.17, + "maxdailygust": 22.82, + "winddir": 11, + "uv": 0, + "solarradiation": 37.64, + "hourlyrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0, + "monthlyrainin": 0, + "totalrainin": 26.402, + "baromrelin": 29.586, + "baromabsin": 28.869, + "batt_co2": 1, + "type": "weather-data", + "dateutc5": 1699474200000, + "lastRain": 1698659100000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station C" + } +} diff --git a/tests/components/ambient_network/fixtures/devices_by_location_response.json b/tests/components/ambient_network/fixtures/devices_by_location_response.json new file mode 100644 index 00000000000..848ba0a7b87 --- /dev/null +++ b/tests/components/ambient_network/fixtures/devices_by_location_response.json @@ -0,0 +1,364 @@ +[ + { + "_id": "aaaaaaaaaaaaaaaaaaaaaaaa", + "macAddress": "AA:AA:AA:AA:AA:AA", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "dateutc": 1699474320000, + "tempf": 82.9, + "humidity": 60, + "windspeedmph": 8.72, + "windgustmph": 9.17, + "maxdailygust": 22.82, + "winddir": 11, + "uv": 0, + "solarradiation": 37.64, + "hourlyrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0, + "monthlyrainin": 0, + "totalrainin": 26.402, + "baromrelin": 29.586, + "baromabsin": 28.869, + "batt_co2": 1, + "type": "weather-data", + "created_at": 1699474320914, + "dateutc5": 1699474200000, + "lastRain": 1698659100000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station A", + "coords": { + "geo": { + "coordinates": [-97.0, 32.0], + "type": "Point" + }, + "elevation": 237.0, + "location": "Location A", + "coords": { + "lon": -97.0, + "lat": 32.0 + } + }, + "indoor": false, + "slug": "aaaaaaaaaaaaaaaaaaaaaaaa" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "bbbbbbbbbbbbbbbbbbbbbbbb", + "macAddress": "BB:BB:BB:BB:BB:BB", + "lastData": { + "stationtype": "AMBWeatherV4.2.6", + "dateutc": 1700716980000, + "baromrelin": 29.342, + "baromabsin": 29.342, + "tempf": 35.8, + "humidity": 88, + "winddir": 237, + "winddir_avg10m": 221, + "windspeedmph": 0, + "windspdmph_avg10m": 0, + "windgustmph": 1.3, + "maxdailygust": 12.3, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0.024, + "monthlyrainin": 0.331, + "yearlyrainin": 12.382, + "solarradiation": 0, + "uv": 0, + "soilhum2": 0, + "type": "weather-data", + "created_at": 1700717004020, + "dateutc5": 1700716800000, + "lastRain": 1700445000000, + "discreets": { + "humidity1": [41, 42, 43] + }, + "tz": "America/Chicago" + }, + "info": { + "name": "Station B", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "location": "Location B", + "elevation": 226.0, + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "bbbbbbbbbbbbbbbbbbbbbbbb" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "cccccccccccccccccccccccc", + "macAddress": "CC:CC:CC:CC:CC:CC", + "lastData": {}, + "info": { + "name": "Station C", + "coords": { + "geo": { + "coordinates": [-97.0, 32.0], + "type": "Point" + }, + "elevation": 242.0, + "location": "Location C", + "coords": { + "lon": -97.0, + "lat": 32.0 + } + }, + "indoor": false, + "slug": "cccccccccccccccccccccccc" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "dddddddddddddddddddddddd", + "macAddress": "DD:DD:DD:DD:DD:DD", + "lastData": { + "stationtype": "AMBWeatherPro_V5.1.3", + "dateutc": 1700716920000, + "tempf": 38.1, + "humidity": 85, + "windspeedmph": 0, + "windgustmph": 0, + "maxdailygust": 0, + "winddir": 89, + "uv": 0, + "solarradiation": 0, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0.028, + "monthlyrainin": 0.327, + "yearlyrainin": 12.76, + "totalrainin": 12.76, + "baromrelin": 29.731, + "baromabsin": 29.338, + "type": "weather-data", + "created_at": 1700716969446, + "dateutc5": 1700716800000, + "lastRain": 1700449500000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station D", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "address": "", + "location": "Location D", + "elevation": 221.0, + "address_components": [ + { + "long_name": "1234", + "short_name": "1234", + "types": ["street_number"] + }, + { + "long_name": "D Street", + "short_name": "D St.", + "types": ["route"] + }, + { + "long_name": "D Town", + "short_name": "D Town", + "types": ["locality", "political"] + }, + { + "long_name": "D County", + "short_name": "D County", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Delaware", + "short_name": "DE", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "United States", + "short_name": "US", + "types": ["country", "political"] + }, + { + "long_name": "12345", + "short_name": "12345", + "types": ["postal_code"] + } + ], + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "dddddddddddddddddddddddd" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "eeeeeeeeeeeeeeeeeeeeeeee", + "macAddress": "EE:EE:EE:EE:EE:EE", + "lastData": { + "stationtype": "AMBWeatherV4.3.4", + "dateutc": 1700716920000, + "baromrelin": 29.238, + "baromabsin": 29.238, + "tempf": 45, + "humidity": 55, + "winddir": 98, + "winddir_avg10m": 185, + "windspeedmph": 1.1, + "windspdmph_avg10m": 1.3, + "windgustmph": 3.4, + "maxdailygust": 12.5, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0.059, + "monthlyrainin": 0.39, + "yearlyrainin": 31.268, + "lightning_day": 1, + "lightning_time": 1700700515000, + "lightning_distance": 8.7, + "batt_lightning": 0, + "solarradiation": 0, + "uv": 0, + "batt_co2": 1, + "type": "weather-data", + "created_at": 1700716954726, + "dateutc5": 1700716800000, + "lastRain": 1700445300000, + "lightnings": [ + [1700713320000, 0], + [1700713380000, 0], + [1700713440000, 0], + [1700713500000, 0], + [1700713560000, 0], + [1700713620000, 0], + [1700713680000, 0], + [1700713740000, 0], + [1700713800000, 0], + [1700713860000, 0], + [1700713920000, 0], + [1700713980000, 0], + [1700714040000, 0], + [1700714100000, 0], + [1700714160000, 0], + [1700714220000, 0], + [1700714280000, 0], + [1700714340000, 0], + [1700714400000, 0], + [1700714460000, 0], + [1700714520000, 0], + [1700714580000, 0], + [1700714640000, 0], + [1700714700000, 0], + [1700714760000, 0], + [1700714820000, 0], + [1700714880000, 0], + [1700714940000, 0], + [1700715000000, 0], + [1700715060000, 0], + [1700715120000, 0], + [1700715180000, 0], + [1700715240000, 0], + [1700715300000, 0], + [1700715360000, 0], + [1700715420000, 0], + [1700715480000, 0], + [1700715540000, 0], + [1700715600000, 0], + [1700715660000, 0], + [1700715720000, 0], + [1700715780000, 0], + [1700715840000, 0], + [1700715900000, 0], + [1700715960000, 0], + [1700716020000, 0], + [1700716080000, 0], + [1700716140000, 0], + [1700716200000, 0], + [1700716260000, 0], + [1700716320000, 0], + [1700716380000, 0], + [1700716440000, 0], + [1700716500000, 0], + [1700716560000, 0], + [1700716620000, 0], + [1700716680000, 0], + [1700716740000, 0], + [1700716800000, 0], + [1700716860000, 0], + [1700716920000, 0] + ], + "lightning_hour": 0, + "tz": "America/Chicago" + }, + "info": { + "name": "Station E", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "location": "Location E", + "elevation": 236.0, + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "eeeeeeeeeeeeeeeeeeeeeeee" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "ffffffffffffffffffffffff", + "macAddress": "FF:FF:FF:FF:FF:FF", + "lastData": {}, + "info": { + "name": "", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "location": "Location F", + "elevation": 242.0, + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "ffffffffffffffffffffffff" + }, + "tz": { + "name": "America/Chicago" + } + } +] diff --git a/tests/components/ambient_network/snapshots/test_sensor.ambr b/tests/components/ambient_network/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fadb15ad015 --- /dev/null +++ b/tests/components/ambient_network/snapshots/test_sensor.ambr @@ -0,0 +1,951 @@ +# serializer version: 1 +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-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.station_a_absolute_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Absolute pressure', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'absolute_pressure', + 'unique_id': 'AA:AA:AA:AA:AA:AA_baromabsin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station A Absolute pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_absolute_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '977.616536580043', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_daily_rain-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.station_a_daily_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_rain', + 'unique_id': 'AA:AA:AA:AA:AA:AA_dailyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_daily_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station A Daily rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_daily_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_dew_point-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.station_a_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': 'AA:AA:AA:AA:AA:AA_dewPoint', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station A Dew point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.7777777777778', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_feels_like-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.station_a_feels_like', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Feels like', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'feels_like', + 'unique_id': 'AA:AA:AA:AA:AA:AA_feelsLike', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_feels_like-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station A Feels like', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_feels_like', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.4444444444444', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_hourly_rain-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.station_a_hourly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hourly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hourly_rain', + 'unique_id': 'AA:AA:AA:AA:AA:AA_hourlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_hourly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'Station A Hourly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_hourly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_humidity-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.station_a_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'AA:AA:AA:AA:AA:AA_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'humidity', + 'friendly_name': 'Station A Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.station_a_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-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.station_a_irradiance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irradiance', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'AA:AA:AA:AA:AA:AA_solarradiation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'irradiance', + 'friendly_name': 'Station A Irradiance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_irradiance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37.64', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-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.station_a_last_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_rain', + 'unique_id': 'AA:AA:AA:AA:AA:AA_lastRain', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'timestamp', + 'friendly_name': 'Station A Last rain', + }), + 'context': , + 'entity_id': 'sensor.station_a_last_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-30T09:45:00+00:00', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_max_daily_gust-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.station_a_max_daily_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max daily gust', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_daily_gust', + 'unique_id': 'AA:AA:AA:AA:AA:AA_maxdailygust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_max_daily_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station A Max daily gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_max_daily_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.72523008', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-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.station_a_monthly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Monthly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'monthly_rain', + 'unique_id': 'AA:AA:AA:AA:AA:AA_monthlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station A Monthly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_monthly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_relative_pressure-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.station_a_relative_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relative pressure', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relative_pressure', + 'unique_id': 'AA:AA:AA:AA:AA:AA_baromrelin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_relative_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station A Relative pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_relative_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1001.89694313129', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_temperature-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.station_a_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'AA:AA:AA:AA:AA:AA_tempf', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station A Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.2777777777778', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_uv_index-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.station_a_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV index', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': 'AA:AA:AA:AA:AA:AA_uv', + 'unit_of_measurement': 'index', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station A UV index', + 'state_class': , + 'unit_of_measurement': 'index', + }), + 'context': , + 'entity_id': 'sensor.station_a_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-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.station_a_weekly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Weekly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'weekly_rain', + 'unique_id': 'AA:AA:AA:AA:AA:AA_weeklyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station A Weekly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_weekly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-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.station_a_wind_direction', + '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': None, + 'original_icon': None, + 'original_name': 'Wind direction', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': 'AA:AA:AA:AA:AA:AA_winddir', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station A Wind direction', + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.station_a_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_gust-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.station_a_wind_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust', + 'unique_id': 'AA:AA:AA:AA:AA:AA_windgustmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station A Wind gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.75768448', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_speed-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.station_a_wind_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'AA:AA:AA:AA:AA:AA_windspeedmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station A Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.03347968', + }) +# --- diff --git a/tests/components/ambient_network/test_config_flow.py b/tests/components/ambient_network/test_config_flow.py new file mode 100644 index 00000000000..d9093de7234 --- /dev/null +++ b/tests/components/ambient_network/test_config_flow.py @@ -0,0 +1,85 @@ +"""Test the Ambient Weather Network config flow.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +from aioambient import OpenAPI +import pytest + +from homeassistant.components.ambient_network.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +async def test_happy_path( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + open_api: OpenAPI, + aioambient: AsyncMock, + devices_by_location: list[dict[str, Any]], + config_entry: ConfigEntry, +) -> None: + """Test the happy path.""" + + setup_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert setup_result["type"] == FlowResultType.FORM + assert setup_result["step_id"] == "user" + + with patch.object( + open_api, + "get_devices_by_location", + AsyncMock(return_value=devices_by_location), + ): + user_result = await hass.config_entries.flow.async_configure( + setup_result["flow_id"], + {"location": {"latitude": 10.0, "longitude": 20.0, "radius": 1.0}}, + ) + + assert user_result["type"] == FlowResultType.FORM + assert user_result["step_id"] == "station" + + stations_result = await hass.config_entries.flow.async_configure( + user_result["flow_id"], + { + "station": "AA:AA:AA:AA:AA:AA", + }, + ) + + assert stations_result["type"] == FlowResultType.CREATE_ENTRY + assert stations_result["title"] == config_entry.title + assert stations_result["data"] == config_entry.data + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_no_station_found( + hass: HomeAssistant, + aioambient: AsyncMock, + open_api: OpenAPI, +) -> None: + """Test that we abort when we cannot find a station in the area.""" + + setup_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert setup_result["type"] == FlowResultType.FORM + assert setup_result["step_id"] == "user" + + with patch.object( + open_api, + "get_devices_by_location", + AsyncMock(return_value=[]), + ): + user_result = await hass.config_entries.flow.async_configure( + setup_result["flow_id"], + {"location": {"latitude": 10.0, "longitude": 20.0, "radius": 1.0}}, + ) + + assert user_result["type"] == FlowResultType.FORM + assert user_result["step_id"] == "user" + assert user_result["errors"] == {"base": "no_stations_found"} diff --git a/tests/components/ambient_network/test_sensor.py b/tests/components/ambient_network/test_sensor.py new file mode 100644 index 00000000000..35aa90ffe05 --- /dev/null +++ b/tests/components/ambient_network/test_sensor.py @@ -0,0 +1,115 @@ +"""Test Ambient Weather Network sensors.""" + +from datetime import datetime, timedelta +from unittest.mock import patch + +from aioambient import OpenAPI +from aioambient.errors import RequestError +from freezegun import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_platform + +from tests.common import async_fire_time_changed, snapshot_platform + + +@freeze_time("2023-11-08") +@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + open_api: OpenAPI, + aioambient, + config_entry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test all sensors under normal operation.""" + await setup_platform(True, hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@freeze_time("2023-11-09") +@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +async def test_sensors_with_stale_data( + hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry +) -> None: + """Test that the sensors are not populated if the data is stale.""" + await setup_platform(False, hass, config_entry) + + sensor = hass.states.get("sensor.station_a_absolute_pressure") + assert sensor is None + + +@freeze_time("2023-11-08") +@pytest.mark.parametrize("config_entry", ["BB:BB:BB:BB:BB:BB"], indirect=True) +async def test_sensors_with_no_data( + hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry +) -> None: + """Test that the sensors are not populated if the last data is absent.""" + await setup_platform(False, hass, config_entry) + + sensor = hass.states.get("sensor.station_b_absolute_pressure") + assert sensor is None + + +@freeze_time("2023-11-08") +@pytest.mark.parametrize("config_entry", ["CC:CC:CC:CC:CC:CC"], indirect=True) +async def test_sensors_with_no_update_time( + hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry +) -> None: + """Test that the sensors are not populated if the update time is missing.""" + await setup_platform(False, hass, config_entry) + + sensor = hass.states.get("sensor.station_c_absolute_pressure") + assert sensor is None + + +@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +async def test_sensors_disappearing( + hass: HomeAssistant, + open_api: OpenAPI, + aioambient, + config_entry, + caplog, +) -> None: + """Test that we log errors properly.""" + + initial_datetime = datetime(year=2023, month=11, day=8) + with freeze_time(initial_datetime) as frozen_datetime: + # Normal state, sensor is available. + await setup_platform(True, hass, config_entry) + sensor = hass.states.get("sensor.station_a_relative_pressure") + assert sensor is not None + assert float(sensor.state) == pytest.approx(1001.89694313129) + + # Sensor becomes unavailable if the network is unavailable. Log message + # should only show up once. + for _ in range(5): + with patch.object( + open_api, "get_device_details", side_effect=RequestError() + ): + frozen_datetime.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.station_a_relative_pressure") + assert sensor is not None + assert sensor.state == "unavailable" + assert caplog.text.count("Cannot connect to Ambient Network") == 1 + + # Network comes back. Sensor should start reporting again. Log message + # should only show up once. + for _ in range(5): + frozen_datetime.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.station_a_relative_pressure") + assert sensor is not None + assert float(sensor.state) == pytest.approx(1001.89694313129) + assert caplog.text.count("Fetching ambient_network data recovered") == 1 diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index ae3af962b0a..19ae9828c22 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch from aioambient.errors import AmbientError import pytest -from homeassistant import data_entry_flow from homeassistant.components.ambient_station import CONF_APP_KEY, DOMAIN 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 @pytest.mark.parametrize( @@ -26,7 +26,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise: @@ -34,14 +34,14 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors # Test that we can recover and finish the flow after errors occur: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" assert result["data"] == { CONF_API_KEY: "12345abcde12345abcde", @@ -56,5 +56,5 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 49ec0ce8d52..77264eb2439 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -63,7 +63,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,7 +71,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Analytics Insights" assert result["data"] == {} assert result["options"] == expected_options @@ -98,7 +98,7 @@ async def test_submitting_empty_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -106,7 +106,7 @@ async def test_submitting_empty_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_integrations_selected"} result = await hass.config_entries.flow.async_configure( @@ -118,7 +118,7 @@ async def test_submitting_empty_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Analytics Insights" assert result["data"] == {} assert result["options"] == { @@ -140,7 +140,7 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -161,8 +161,8 @@ async def test_form_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" @pytest.mark.parametrize( @@ -209,7 +209,7 @@ async def test_options_flow( await setup_integration(hass, mock_config_entry) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM mock_analytics_client.get_integrations.reset_mock() result = await hass.config_entries.options.async_configure( @@ -218,7 +218,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == expected_options await hass.async_block_till_done() mock_analytics_client.get_integrations.assert_called_once() @@ -244,7 +244,7 @@ async def test_submitting_empty_options_flow( await setup_integration(hass, mock_config_entry) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -252,7 +252,7 @@ async def test_submitting_empty_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_integrations_selected"} result = await hass.config_entries.options.async_configure( @@ -264,7 +264,7 @@ async def test_submitting_empty_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], @@ -285,5 +285,5 @@ async def test_options_flow_cannot_connect( mock_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/analytics_insights/test_init.py b/tests/components/analytics_insights/test_init.py index 4f1ca7cda95..8543a02c025 100644 --- a/tests/components/analytics_insights/test_init.py +++ b/tests/components/analytics_insights/test_init.py @@ -21,9 +21,9 @@ async def test_load_unload_entry( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/analytics_insights/test_sensor.py b/tests/components/analytics_insights/test_sensor.py index e0850bbd55b..3ede971c8f8 100644 --- a/tests/components/analytics_insights/test_sensor.py +++ b/tests/components/analytics_insights/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_all_entities( @@ -32,17 +32,10 @@ async def test_all_entities( [Platform.SENSOR], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, 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") - async def test_connection_error( hass: HomeAssistant, diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py index 2e4522188eb..6e6e34fb9f8 100644 --- a/tests/components/android_ip_webcam/test_config_flow.py +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -55,7 +55,7 @@ async def test_device_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -66,7 +66,7 @@ async def test_device_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -87,7 +87,7 @@ async def test_form_invalid_auth( {"host": "1.1.1.1", "port": 8080, "username": "user", "password": "wrong-pass"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"username": "invalid_auth", "password": "invalid_auth"} @@ -110,5 +110,5 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py index 9aa677b8708..70ecdc9271e 100644 --- a/tests/components/android_ip_webcam/test_init.py +++ b/tests/components/android_ip_webcam/test_init.py @@ -30,7 +30,7 @@ async def test_successful_config_entry( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_setup_failed_connection_error( @@ -47,7 +47,7 @@ async def test_setup_failed_connection_error( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_failed_invalid_auth( @@ -64,7 +64,7 @@ async def test_setup_failed_invalid_auth( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 67393a21f41..90a13523ebe 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from androidtv.adb_manager.adb_manager_async import DeviceAsync from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0 from homeassistant.components.androidtv.const import ( @@ -62,7 +63,7 @@ class ClientAsyncFakeFail: """Initialize a `ClientAsyncFakeFail` instance.""" self._devices = [] - async def device(self, serial): + async def device(self, serial) -> DeviceAsync | None: """Mock the `ClientAsync.device` method when the device is not connected via ADB.""" self._devices = [] return None diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index afebe9903ce..e2b5207c590 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant import data_entry_flow from homeassistant.components.androidtv.config_flow import ( APPS_NEW_ID, CONF_APP_DELETE, @@ -37,6 +36,7 @@ from homeassistant.components.androidtv.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .patchers import PATCH_ACCESS, PATCH_ISFILE, PATCH_SETUP_ENTRY @@ -104,7 +104,7 @@ async def test_user( flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.FlowResultType.FORM + assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -120,7 +120,7 @@ async def test_user( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config @@ -148,7 +148,7 @@ async def test_user_adbkey(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config_data @@ -166,7 +166,7 @@ async def test_error_both_key_server(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "key_and_server"} with ( @@ -181,7 +181,7 @@ async def test_error_both_key_server(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -196,7 +196,7 @@ async def test_error_invalid_key(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "adbkey_not_file"} with ( @@ -211,7 +211,7 @@ async def test_error_invalid_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -244,7 +244,7 @@ async def test_invalid_mac( data=config, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -262,7 +262,7 @@ async def test_abort_if_host_exist(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -285,7 +285,7 @@ async def test_abort_if_unique_exist(hass: HomeAssistant) -> None: data=CONFIG_ADB_SERVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -300,7 +300,7 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -310,7 +310,7 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with ( @@ -325,7 +325,7 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == HOST assert result3["data"] == CONFIG_ADB_SERVER @@ -348,7 +348,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test app form with existing app @@ -358,7 +358,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "apps" # test change value in apps form @@ -368,7 +368,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APP_NAME: "Appl1", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test app form with new app @@ -378,7 +378,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APPS: APPS_NEW_ID, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "apps" # test save value for new app @@ -389,7 +389,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APP_NAME: "Appl2", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test app form for delete @@ -399,7 +399,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "apps" # test delete app1 @@ -410,7 +410,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APP_DELETE: True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test rules form with existing rule @@ -420,7 +420,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" # test change value in rule form with invalid json rule @@ -430,7 +430,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: "a", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -441,7 +441,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: {"a": "b"}, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -452,7 +452,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: ["standby"], }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test rule form with new rule @@ -462,7 +462,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATE_DETECTION_RULES: RULES_NEW_ID, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" # test save value for new rule @@ -473,7 +473,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: VALID_DETECT_RULE, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test rules form with delete existing rule @@ -483,7 +483,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" # test delete rule @@ -493,7 +493,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_DELETE: True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -507,7 +507,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY apps_options = config_entry.options[CONF_APPS] assert apps_options.get("app1") is None diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 63923a57996..fe6b9962d14 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -591,7 +591,7 @@ async def test_setup_fail( await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert state is None diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index f4e141ce952..062b9a4a55c 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -25,7 +25,7 @@ async def test_user_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -44,7 +44,7 @@ async def test_user_flow_success( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -58,7 +58,7 @@ async def test_user_flow_success( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == {"host": host, "name": name, "mac": mac} assert result["context"]["source"] == "user" @@ -85,7 +85,7 @@ async def test_user_flow_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -99,7 +99,7 @@ async def test_user_flow_cannot_connect( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert result["errors"] == {"base": "cannot_connect"} @@ -127,7 +127,7 @@ async def test_user_flow_pairing_invalid_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -145,7 +145,7 @@ async def test_user_flow_pairing_invalid_auth( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -159,7 +159,7 @@ async def test_user_flow_pairing_invalid_auth( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert result["errors"] == {"base": "invalid_auth"} @@ -189,7 +189,7 @@ async def test_user_flow_pairing_connection_closed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -207,7 +207,7 @@ async def test_user_flow_pairing_connection_closed( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -221,7 +221,7 @@ async def test_user_flow_pairing_connection_closed( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -251,7 +251,7 @@ async def test_user_flow_pairing_connection_closed_followed_by_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -269,7 +269,7 @@ async def test_user_flow_pairing_connection_closed_followed_by_cannot_connect( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -283,7 +283,7 @@ async def test_user_flow_pairing_connection_closed_followed_by_cannot_connect( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" mock_api.async_finish_pairing.assert_called_with(pin) @@ -324,11 +324,12 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -340,7 +341,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( result["flow_id"], {"host": host} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" mock_api.async_generate_cert_if_missing.assert_called() @@ -387,7 +388,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -399,7 +400,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry( result["flow_id"], {"host": host} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" mock_api.async_generate_cert_if_missing.assert_called() @@ -442,7 +443,7 @@ async def test_zeroconf_flow_success( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert not result["data_schema"] @@ -461,7 +462,7 @@ async def test_zeroconf_flow_success( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -475,7 +476,7 @@ async def test_zeroconf_flow_success( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == { "host": host, @@ -520,7 +521,7 @@ async def test_zeroconf_flow_cannot_connect( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert not result["data_schema"] @@ -531,7 +532,7 @@ async def test_zeroconf_flow_cannot_connect( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" mock_api.async_generate_cert_if_missing.assert_called() @@ -571,7 +572,7 @@ async def test_zeroconf_flow_pairing_invalid_auth( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert not result["data_schema"] @@ -582,7 +583,7 @@ async def test_zeroconf_flow_pairing_invalid_auth( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -596,7 +597,7 @@ async def test_zeroconf_flow_pairing_invalid_auth( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert result["errors"] == {"base": "invalid_auth"} @@ -640,6 +641,7 @@ async def test_zeroconf_flow_already_configured_host_changed_reloads_entry( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -654,7 +656,7 @@ async def test_zeroconf_flow_already_configured_host_changed_reloads_entry( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -707,7 +709,7 @@ async def test_zeroconf_flow_already_configured_host_not_changed_no_reload_entry properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -740,7 +742,7 @@ async def test_zeroconf_flow_abort_if_mac_is_missing( properties={}, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -769,6 +771,7 @@ async def test_reauth_flow_success( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() @@ -785,7 +788,7 @@ async def test_reauth_flow_success( mock_api.async_start_pairing = AsyncMock(return_value=None) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -799,7 +802,7 @@ async def test_reauth_flow_success( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_api.async_finish_pairing.assert_called_with(pin) @@ -854,7 +857,7 @@ async def test_reauth_flow_cannot_connect( mock_api.async_start_pairing = AsyncMock(side_effect=CannotConnect()) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -880,7 +883,7 @@ async def test_options_flow( # Trigger options flow, first time result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"enable_ime"} @@ -889,7 +892,7 @@ async def test_options_flow( result["flow_id"], user_input={"enable_ime": False}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == {"enable_ime": False} await hass.async_block_till_done() @@ -902,7 +905,7 @@ async def test_options_flow( result["flow_id"], user_input={"enable_ime": False}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == {"enable_ime": False} await hass.async_block_till_done() @@ -915,7 +918,7 @@ async def test_options_flow( result["flow_id"], user_input={"enable_ime": True}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == {"enable_ime": True} await hass.async_block_till_done() diff --git a/tests/components/anova/test_config_flow.py b/tests/components/anova/test_config_flow.py index 6ea988dc53a..b92c50c40b0 100644 --- a/tests/components/anova/test_config_flow.py +++ b/tests/components/anova/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from anova_wifi import AnovaPrecisionCooker, InvalidLogin, NoDevicesFound -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.anova.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONF_INPUT, DEVICE_UNIQUE_ID, create_entry @@ -41,7 +42,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "sample@gmail.com", CONF_PASSWORD: "sample", @@ -76,7 +77,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -94,7 +95,7 @@ async def test_flow_wrong_login(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -112,7 +113,7 @@ async def test_flow_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -133,5 +134,5 @@ async def test_flow_no_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_devices_found"} diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index a0a6bf82762..ee2f1da00e9 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form_with_valid_connection( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form_with_valid_connection( await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Anthem AV" assert result2["data"] == { "host": "1.1.1.1", @@ -67,7 +67,7 @@ async def test_form_device_info_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} @@ -91,7 +91,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -112,5 +112,5 @@ async def test_device_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 6989ffc69c5..45614a1d885 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import ANY, AsyncMock, patch from anthemav.device_error import DeviceError import pytest -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -24,13 +24,13 @@ async def test_load_unload_config_entry( mock_connection_create.assert_called_with( host="1.1.1.1", port=14999, update_callback=ANY ) - assert init_integration.state == config_entries.ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED # unload await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() # assert unload and avr is closed - assert init_integration.state == config_entries.ConfigEntryState.NOT_LOADED + assert init_integration.state is ConfigEntryState.NOT_LOADED mock_anthemav.close.assert_called_once() @@ -46,7 +46,7 @@ async def test_config_entry_not_ready_when_oserror( mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_anthemav_dispatcher_signal( diff --git a/tests/components/aosmith/snapshots/test_sensor.ambr b/tests/components/aosmith/snapshots/test_sensor.ambr index 150e0c2934f..7aae9713037 100644 --- a/tests/components/aosmith/snapshots/test_sensor.ambr +++ b/tests/components/aosmith/snapshots/test_sensor.ambr @@ -1,5 +1,43 @@ # serializer version: 1 -# name: test_state[sensor.my_water_heater_energy_usage] +# name: test_state[sensor.my_water_heater_energy_usage-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.my_water_heater_energy_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy usage', + 'platform': 'aosmith', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_usage', + 'unique_id': 'energy_usage_junctionId', + 'unit_of_measurement': , + }) +# --- +# name: test_state[sensor.my_water_heater_energy_usage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -15,7 +53,46 @@ 'state': '132.825', }) # --- -# name: test_state[sensor.my_water_heater_hot_water_availability] +# name: test_state[sensor.my_water_heater_hot_water_availability-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'medium', + 'high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_water_heater_hot_water_availability', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water availability', + 'platform': 'aosmith', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hot_water_availability', + 'unique_id': 'hot_water_availability_junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[sensor.my_water_heater_hot_water_availability-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'enum', diff --git a/tests/components/aosmith/snapshots/test_water_heater.ambr b/tests/components/aosmith/snapshots/test_water_heater.ambr index c3740341c17..deb079570f1 100644 --- a/tests/components/aosmith/snapshots/test_water_heater.ambr +++ b/tests/components/aosmith/snapshots/test_water_heater.ambr @@ -1,5 +1,103 @@ # serializer version: 1 -# name: test_state +# name: test_state[False][water_heater.my_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 130, + 'min_temp': 95, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.my_water_heater', + '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': 'aosmith', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[False][water_heater.my_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'away_mode': 'off', + 'current_temperature': None, + 'friendly_name': 'My water heater', + 'max_temp': 130, + 'min_temp': 95, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 130, + }), + 'context': , + 'entity_id': 'water_heater.my_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'electric', + }) +# --- +# name: test_state[True][water_heater.my_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 130, + 'min_temp': 95, + 'operation_list': list([ + 'electric', + 'eco', + 'heat_pump', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.my_water_heater', + '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': 'aosmith', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[True][water_heater.my_water_heater-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'away_mode': 'off', @@ -26,24 +124,3 @@ 'state': 'heat_pump', }) # --- -# name: test_state_non_heat_pump[False] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'away_mode': 'off', - 'current_temperature': None, - 'friendly_name': 'My water heater', - 'max_temp': 130, - 'min_temp': 95, - 'supported_features': , - 'target_temp_high': None, - 'target_temp_low': None, - 'temperature': 130, - }), - '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 32f259f552c..991d4129392 100644 --- a/tests/components/aosmith/test_config_flow.py +++ b/tests/components/aosmith/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +63,7 @@ async def test_form_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -74,7 +74,7 @@ async def test_form_exception( result["flow_id"], FIXTURE_USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": expected_error_key} with patch( @@ -87,7 +87,7 @@ async def test_form_exception( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] assert result3["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -178,7 +178,7 @@ async def test_reauth_flow_retry( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} # Second attempt at reauth - authentication succeeds @@ -195,5 +195,5 @@ async def test_reauth_flow_retry( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/aosmith/test_sensor.py b/tests/components/aosmith/test_sensor.py index f94dfdb710c..d6acd8865d8 100644 --- a/tests/components/aosmith/test_sensor.py +++ b/tests/components/aosmith/test_sensor.py @@ -1,50 +1,30 @@ """Tests for the sensor platform of the A. O. Smith integration.""" +from collections.abc import AsyncGenerator +from unittest.mock import patch + import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - ("entity_id", "unique_id"), - [ - ( - "sensor.my_water_heater_hot_water_availability", - "hot_water_availability_junctionId", - ), - ("sensor.my_water_heater_energy_usage", "energy_usage_junctionId"), - ], -) -async def test_setup( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - init_integration: MockConfigEntry, - entity_id: str, - unique_id: str, -) -> None: - """Test the setup of the sensor entities.""" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.unique_id == unique_id +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[list[str], None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.SENSOR]): + yield -@pytest.mark.parametrize( - ("entity_id"), - [ - "sensor.my_water_heater_hot_water_availability", - "sensor.my_water_heater_energy_usage", - ], -) async def test_state( hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion, - entity_id: str, + entity_registry: er.EntityRegistry, ) -> None: """Test the state of the sensor entities.""" - state = hass.states.get(entity_id) - assert state == snapshot + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) diff --git a/tests/components/aosmith/test_water_heater.py b/tests/components/aosmith/test_water_heater.py index a256f720c0a..567121ac0b0 100644 --- a/tests/components/aosmith/test_water_heater.py +++ b/tests/components/aosmith/test_water_heater.py @@ -1,6 +1,7 @@ """Tests for the water heater platform of the A. O. Smith integration.""" -from unittest.mock import MagicMock +from collections.abc import AsyncGenerator +from unittest.mock import MagicMock, patch from py_aosmith.models import OperationMode import pytest @@ -19,53 +20,33 @@ from homeassistant.components.water_heater import ( STATE_HEAT_PUMP, WaterHeaterEntityFeature, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - ATTR_SUPPORTED_FEATURES, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -async def test_setup( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - init_integration: MockConfigEntry, -) -> None: - """Test the setup of the water heater entity.""" - entry = entity_registry.async_get("water_heater.my_water_heater") - assert entry - assert entry.unique_id == "junctionId" - - state = hass.states.get("water_heater.my_water_heater") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My water heater" - - -async def test_state( - hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion -) -> None: - """Test the state of the water heater entity.""" - state = hass.states.get("water_heater.my_water_heater") - assert state == snapshot +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[list[str], None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.WATER_HEATER]): + yield @pytest.mark.parametrize( ("get_devices_fixture_heat_pump"), - [ - False, - ], + [False, True], ) -async def test_state_non_heat_pump( - hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion +async def test_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: - """Test the state of the water heater entity for a non heat pump device.""" - state = hass.states.get("water_heater.my_water_heater") - assert state == snapshot + """Test the state of the water heater entities.""" + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index 93d9619f0c3..2b702046054 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.apache_kafka as apache_kafka +from homeassistant.components import apache_kafka from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index bed0e78ad55..2888771eb01 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" @@ -63,7 +63,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Then, we create the integration once again using a different port. However, @@ -78,7 +78,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=another_host, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Now we change the serial number and add it again. This should be successful. @@ -91,7 +91,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=another_host, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == another_host @@ -105,14 +105,14 @@ async def test_flow_works(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_STATUS["UPSNAME"] assert result["data"] == CONF_DATA @@ -147,7 +147,7 @@ async def test_flow_minimal_status( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA assert result["title"] == expected_title mock_setup.assert_called_once() diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 0ac2e5973fe..5443d48452f 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -306,7 +306,7 @@ async def test_api_get_services( for serv_domain in data: local = local_services.pop(serv_domain["domain"]) - assert serv_domain["services"] == local + assert serv_domain["services"].keys() == local.keys() async def test_api_call_service_no_data( diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 28d87ef1b03..e7bfa68bdaf 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -7,7 +7,7 @@ from pyatv import exceptions from pyatv.const import PairingRequirement, Protocol import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.apple_tv import CONF_ADDRESS, config_flow from homeassistant.components.apple_tv.const import ( @@ -16,6 +16,7 @@ from homeassistant.components.apple_tv.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import airplay_service, create_conf, mrp_service, raop_service @@ -72,7 +73,7 @@ async def test_user_input_device_not_found(hass: HomeAssistant, mrp_device) -> N 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -80,7 +81,7 @@ async def test_user_input_device_not_found(hass: HomeAssistant, mrp_device) -> N {"device_input": "none"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_devices_found"} @@ -96,7 +97,7 @@ async def test_user_input_unexpected_error(hass: HomeAssistant, mock_scan) -> No {"device_input": "dummy"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -105,37 +106,37 @@ async def test_user_adds_full_device(hass: HomeAssistant, full_device, pairing) 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"device_input": "MRP Device"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["description_placeholders"] == {"protocol": "MRP"} result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": "1111"} result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == data_entry_flow.FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["description_placeholders"] == {"protocol": "AirPlay"} result6 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1234} ) - assert result6["type"] == "create_entry" + assert result6["type"] is FlowResultType.CREATE_ENTRY assert result6["data"] == { "address": "127.0.0.1", "credentials": { @@ -160,20 +161,20 @@ async def test_user_adds_dmap_device( result["flow_id"], {"device_input": "DMAP Device"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["description_placeholders"] == {"pin": "1111", "protocol": "DMAP"} result6 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1234} ) - assert result6["type"] == "create_entry" + assert result6["type"] is FlowResultType.CREATE_ENTRY assert result6["data"] == { "address": "127.0.0.1", "credentials": {Protocol.DMAP.value: "dmap_creds"}, @@ -200,7 +201,7 @@ async def test_user_adds_dmap_device_failed( await hass.config_entries.flow.async_configure(result["flow_id"], {}) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "device_did_not_pair" @@ -216,7 +217,7 @@ async def test_user_adds_device_with_ip_filter( result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -277,7 +278,7 @@ async def test_user_adds_existing_device(hass: HomeAssistant, mrp_device) -> Non result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -305,7 +306,7 @@ async def test_user_connection_failed( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -328,7 +329,7 @@ async def test_user_start_pair_error_failed( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "invalid_auth" @@ -349,14 +350,14 @@ async def test_user_pair_service_with_password( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "password" result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "setup_failed" @@ -378,14 +379,14 @@ async def test_user_pair_disabled_service( result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol_disabled" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -407,7 +408,7 @@ async def test_user_pair_ignore_unsupported( result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -435,7 +436,7 @@ async def test_user_pair_invalid_pin( result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -463,7 +464,7 @@ async def test_user_pair_unexpected_error( result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -486,7 +487,7 @@ async def test_user_pair_backoff_error( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "backoff" @@ -509,7 +510,7 @@ async def test_user_pair_begin_unexpected_error( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "unknown" @@ -526,20 +527,20 @@ async def test_ignores_disabled_service( result["flow_id"], {"device_input": "mrpid"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { "name": "AirPlay Device", "type": "Unknown", } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "AirPlay"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "address": "127.0.0.1", "credentials": { @@ -568,7 +569,7 @@ async def test_zeroconf_unsupported_service_aborts(hass: HomeAssistant) -> None: properties={}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -589,7 +590,7 @@ async def test_zeroconf_add_mrp_device( type="_mediaremotetv._tcp.local.", ), ) - assert unrelated_result["type"] == data_entry_flow.FlowResultType.FORM + assert unrelated_result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, @@ -604,7 +605,7 @@ async def test_zeroconf_add_mrp_device( type="_mediaremotetv._tcp.local.", ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", @@ -614,13 +615,13 @@ async def test_zeroconf_add_mrp_device( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "address": "127.0.0.1", "credentials": {Protocol.MRP.value: "mrp_creds"}, @@ -636,7 +637,7 @@ async def test_zeroconf_add_dmap_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -646,11 +647,11 @@ async def test_zeroconf_add_dmap_device( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": "1111"} result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "address": "127.0.0.1", "credentials": {Protocol.DMAP.value: "dmap_creds"}, @@ -685,7 +686,7 @@ async def test_zeroconf_ip_change(hass: HomeAssistant, mock_scan) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -723,7 +724,7 @@ async def test_zeroconf_ip_change_after_ip_conflict_with_ignored_entry( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 1 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -764,7 +765,7 @@ async def test_zeroconf_ip_change_via_secondary_identifier( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -807,7 +808,7 @@ async def test_zeroconf_updates_identifiers_for_ignored_entries( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert ( len(mock_async_setup.mock_calls) == 0 @@ -826,7 +827,7 @@ async def test_zeroconf_add_existing_aborts(hass: HomeAssistant, dmap_device) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -837,7 +838,7 @@ async def test_zeroconf_add_but_device_not_found( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -848,7 +849,7 @@ async def test_zeroconf_add_existing_device(hass: HomeAssistant, dmap_device) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -859,7 +860,7 @@ async def test_zeroconf_unexpected_error(hass: HomeAssistant, mock_scan) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -885,7 +886,7 @@ async def test_zeroconf_abort_if_other_in_progress( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" mock_scan.result = [ @@ -907,7 +908,7 @@ async def test_zeroconf_abort_if_other_in_progress( properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -965,7 +966,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "device_not_found" @@ -1025,7 +1026,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "inconsistent_device" @@ -1051,7 +1052,7 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await hass.async_block_till_done() mock_scan.result = [ @@ -1102,7 +1103,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pair_no_pin" assert result2["description_placeholders"] == {"pin": ANY, "protocol": "RAOP"} @@ -1112,7 +1113,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "pair_with_pin" assert result3["description_placeholders"] == {"protocol": "MRP"} @@ -1120,7 +1121,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "pair_with_pin" assert result4["description_placeholders"] == {"protocol": "AirPlay"} @@ -1128,7 +1129,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_mismatch( @@ -1157,14 +1158,14 @@ async def test_zeroconf_mismatch( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -1190,13 +1191,13 @@ async def test_reconfigure_update_credentials( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data == { @@ -1218,12 +1219,12 @@ async def test_option_start_off(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_START_OFF: True} ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_START_OFF] @@ -1243,5 +1244,5 @@ async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: properties={"CtlN": "Apple TV"}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" diff --git a/tests/components/apple_tv/test_remote.py b/tests/components/apple_tv/test_remote.py index f831518d75a..bc8a0e6a2dd 100644 --- a/tests/components/apple_tv/test_remote.py +++ b/tests/components/apple_tv/test_remote.py @@ -5,25 +5,37 @@ from unittest.mock import AsyncMock import pytest from homeassistant.components.apple_tv.remote import AppleTVRemote -from homeassistant.components.remote import ATTR_DELAY_SECS, ATTR_NUM_REPEATS +from homeassistant.components.remote import ( + ATTR_DELAY_SECS, + ATTR_HOLD_SECS, + ATTR_NUM_REPEATS, +) @pytest.mark.parametrize( - ("command", "method"), + ("command", "method", "hold_secs"), [ - ("up", "remote_control.up"), - ("wakeup", "power.turn_on"), - ("volume_up", "audio.volume_up"), - ("home_hold", "remote_control.home"), + ("up", "remote_control.up", 0.0), + ("wakeup", "power.turn_on", 0.0), + ("volume_up", "audio.volume_up", 0.0), + ("home", "remote_control.home", 1.0), + ("select", "remote_control.select", 1.0), ], - ids=["up", "wakeup", "volume_up", "home_hold"], + ids=["up", "wakeup", "volume_up", "home", "select"], ) -async def test_send_command(command: str, method: str) -> None: +async def test_send_command(command: str, method: str, hold_secs: float) -> None: """Test "send_command" method.""" remote = AppleTVRemote("test", "test", None) remote.atv = AsyncMock() await remote.async_send_command( - [command], **{ATTR_NUM_REPEATS: 1, ATTR_DELAY_SECS: 0} + [command], + **{ATTR_NUM_REPEATS: 1, ATTR_DELAY_SECS: 0, ATTR_HOLD_SECS: hold_secs}, ) assert len(remote.atv.method_calls) == 1 - assert str(remote.atv.method_calls[0]) == f"call.{method}()" + if hold_secs >= 1: + assert ( + str(remote.atv.method_calls[0]) + == f"call.{method}(action=)" + ) + else: + assert str(remote.atv.method_calls[0]) == f"call.{method}()" diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 2d44aec4461..523abc7fd84 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -26,6 +26,7 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -163,7 +164,7 @@ class OAuthFixture: ) result = await self.hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == self.title assert "data" in result assert "token" in result["data"] @@ -188,7 +189,9 @@ class Client: self.client = client self.id = 0 - async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]: + async def cmd( + self, cmd: str, payload: dict[str, Any] | None = None + ) -> dict[str, Any]: """Send a command and receive the json result.""" self.id += 1 await self.client.send_json( @@ -202,7 +205,7 @@ class Client: assert resp.get("id") == self.id return resp - async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any: + async def cmd_result(self, cmd: str, payload: dict[str, Any] | None = None) -> Any: """Send a command and parse the result.""" resp = await self.cmd(cmd, payload) assert resp.get("success") @@ -420,7 +423,7 @@ async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -447,7 +450,7 @@ async def test_config_flow_other_domain( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -470,7 +473,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP result = await oauth_fixture.complete_external_step(result) assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id" @@ -535,14 +538,14 @@ async def test_config_flow_multiple_entries( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pick_implementation" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"implementation": "fake_integration_some_client_id2"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.client_id = CLIENT_ID + "2" oauth_fixture.title = CLIENT_ID + "2" result = await oauth_fixture.complete_external_step(result) @@ -572,7 +575,7 @@ async def test_config_flow_create_delete_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -589,7 +592,7 @@ async def test_config_flow_with_config_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -607,7 +610,7 @@ async def test_import_without_setup(hass: HomeAssistant, config_credential) -> N result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -636,7 +639,7 @@ async def test_websocket_without_platform( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -711,7 +714,7 @@ async def test_platform_with_auth_implementation( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -769,7 +772,7 @@ async def test_name( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.title = NAME result = await oauth_fixture.complete_external_step(result) assert ( diff --git a/tests/components/aprilaire/test_config_flow.py b/tests/components/aprilaire/test_config_flow.py index 6508379665b..c9cba2b3fd6 100644 --- a/tests/components/aprilaire/test_config_flow.py +++ b/tests/components/aprilaire/test_config_flow.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.aprilaire.config_flow import ( STEP_USER_DATA_SCHEMA, - ConfigFlow, + AprilaireConfigFlow, ) from homeassistant.core import HomeAssistant @@ -24,7 +24,7 @@ async def test_user_input_step() -> None: show_form_mock = Mock() - config_flow = ConfigFlow() + config_flow = AprilaireConfigFlow() config_flow.async_show_form = show_form_mock await config_flow.async_step_user(None) @@ -41,7 +41,7 @@ async def test_config_flow_invalid_data(client: AprilaireClient) -> None: set_unique_id_mock = AsyncMock() async_abort_entries_match_mock = Mock() - config_flow = ConfigFlow() + config_flow = AprilaireConfigFlow() config_flow.async_show_form = show_form_mock config_flow.async_set_unique_id = set_unique_id_mock config_flow._async_abort_entries_match = async_abort_entries_match_mock @@ -77,7 +77,7 @@ async def test_config_flow_data(client: AprilaireClient, hass: HomeAssistant) -> abort_if_unique_id_configured_mock = Mock() create_entry_mock = Mock() - config_flow = ConfigFlow() + config_flow = AprilaireConfigFlow() config_flow.hass = hass config_flow.async_show_form = show_form_mock config_flow.async_set_unique_id = set_unique_id_mock diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index ca2a5ce1833..92081111c8b 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -7,7 +7,7 @@ import aprslib from aprslib import IS import pytest -import homeassistant.components.aprs.device_tracker as device_tracker +from homeassistant.components.aprs import device_tracker from homeassistant.core import HomeAssistant DEFAULT_PORT = 14580 diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py index a779a93cd8f..9596507960b 100644 --- a/tests/components/aranet/test_config_flow.py +++ b/tests/components/aranet/test_config_flow.py @@ -25,13 +25,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aranet4 12345" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -63,7 +63,7 @@ async def test_async_step_bluetooth_not_aranet4(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_ARANET4_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) -> None: @@ -79,7 +79,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +90,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -98,7 +98,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -111,7 +111,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -122,14 +122,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aranet4 12345" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -144,7 +144,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -158,7 +158,7 @@ async def test_async_step_user_only_other_devices_found(hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -172,14 +172,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aranet4 12345" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -195,7 +195,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -209,7 +209,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -231,7 +231,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -245,14 +245,14 @@ async def test_async_step_user_old_firmware(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "outdated_version" @@ -266,12 +266,12 @@ async def test_async_step_user_integrations_disabled(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "integrations_disabled" diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 470a91feb3b..65991c313ee 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -6,13 +6,13 @@ from unittest.mock import AsyncMock, patch from arcam.fmj.client import ConnectionFailed import pytest -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.arcam_fmj.config_flow import get_entry_client from homeassistant.components.arcam_fmj.const import DOMAIN, DOMAIN_DATA_ENTRIES from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_CONFIG_ENTRY, @@ -68,11 +68,11 @@ async def test_ssdp(hass: HomeAssistant, dummy_client) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY @@ -89,7 +89,7 @@ async def test_ssdp_abort(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -102,11 +102,11 @@ async def test_ssdp_unable_to_connect(hass: HomeAssistant, dummy_client) -> None context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -121,7 +121,7 @@ async def test_ssdp_invalid_id(hass: HomeAssistant, dummy_client) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=discover, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -140,7 +140,7 @@ async def test_ssdp_update(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_HOST @@ -155,7 +155,7 @@ async def test_user(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -167,7 +167,7 @@ async def test_user(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id == MOCK_UUID @@ -188,7 +188,7 @@ async def test_invalid_ssdp( context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id is None @@ -209,7 +209,7 @@ async def test_user_wrong( context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["result"].unique_id is None diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index a8510628b26..1b43d27281c 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -2,8 +2,8 @@ import pytest +from homeassistant.components import automation from homeassistant.components.arcam_fmj.const import DOMAIN -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/arve/__init__.py b/tests/components/arve/__init__.py new file mode 100644 index 00000000000..24f970b55b6 --- /dev/null +++ b/tests/components/arve/__init__.py @@ -0,0 +1,20 @@ +"""Tests for the Arve integration.""" + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +USER_INPUT = { + CONF_ACCESS_TOKEN: "test-access-token", + CONF_CLIENT_SECRET: "test-customer-token", +} + + +async def async_init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the Arve integration for testing.""" + 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/arve/conftest.py b/tests/components/arve/conftest.py new file mode 100644 index 00000000000..f1dfee8ba41 --- /dev/null +++ b/tests/components/arve/conftest.py @@ -0,0 +1,56 @@ +"""Common fixtures for the Arve tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from asyncarve import ArveCustomer, ArveDevices, ArveSensPro, ArveSensProData +import pytest + +from homeassistant.components.arve.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import USER_INPUT + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.arve.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant, mock_arve: MagicMock) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Arve", domain=DOMAIN, data=USER_INPUT, unique_id=mock_arve.customer_id + ) + + +@pytest.fixture +def mock_arve(): + """Return a mocked Arve client.""" + + with ( + patch( + "homeassistant.components.arve.coordinator.Arve", autospec=True + ) as arve_mock, + patch("homeassistant.components.arve.config_flow.Arve", new=arve_mock), + ): + arve = arve_mock.return_value + arve.customer_id = 12345 + + arve.get_customer_id.return_value = ArveCustomer(12345) + + arve.get_devices.return_value = ArveDevices(["test-serial-number"]) + arve.get_sensor_info.return_value = ArveSensPro("Test Sensor", "1.0", "prov1") + + arve.device_sensor_data.return_value = ArveSensProData( + 14, 595.75, 28.71, 0.16, 0.19, 26.02, 7 + ) + + yield arve diff --git a/tests/components/arve/snapshots/test_sensor.ambr b/tests/components/arve/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5c5c4c84d08 --- /dev/null +++ b/tests/components/arve/snapshots/test_sensor.ambr @@ -0,0 +1,773 @@ +# serializer version: 1 +# name: test_sensors[entry_air_quality_index] + 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.test_sensor_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air quality index', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_AQI', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_carbon_dioxide] + 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.test_sensor_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_CO2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[entry_humidity] + 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.test_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[entry_pm10] + 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.test_sensor_pm10', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_pm2_5] + 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.test_sensor_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM25', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_temperature] + 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.test_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[entry_test-serial-number_air_quality_index] + 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.test_sensor_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air quality index', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_AQI', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_test-serial-number_carbon_dioxide] + 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.test_sensor_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_CO2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[entry_test-serial-number_humidity] + 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.test_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[entry_test-serial-number_none] + 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.my_arve_none', + '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': 'TVOC', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_tvoc', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_test-serial-number_pm10] + 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.test_sensor_pm10', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_test-serial-number_pm2_5] + 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.test_sensor_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM25', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_test-serial-number_temperature] + 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.test_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[entry_test-serial-number_total_volatile_organic_compounds] + 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.test_sensor_total_volatile_organic_compounds', + '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': 'Total volatile organic compounds', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_TVOC', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_test-serial-number_tvoc] + 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.my_arve_tvoc', + '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': 'TVOC', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_tvoc', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_total_volatile_organic_compounds] + 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.test_sensor_total_volatile_organic_compounds', + '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': 'Total volatile organic compounds', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_TVOC', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[my_arve_air_quality_index] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'aqi', + 'friendly_name': 'My Arve AQI', + }), + 'context': , + 'entity_id': 'sensor.my_arve_air_quality_index', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_carbon_dioxide] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'My Arve CO2', + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.my_arve_carbon_dioxide', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_humidity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'My Arve Humidity', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.my_arve_humidity', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_none] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My Arve TVOC', + }), + 'context': , + 'entity_id': 'sensor.my_arve_none', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_pm10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'My Arve PM10', + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.my_arve_pm10', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_pm2_5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'My Arve PM25', + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.my_arve_pm2_5', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_temperature] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'My Arve Temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_arve_temperature', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_tvoc] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My Arve TVOC', + }), + 'context': , + 'entity_id': 'sensor.my_arve_tvoc', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[test_sensor_air_quality_index] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'aqi', + 'friendly_name': 'Test Sensor Air quality index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.test_sensor_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14', + }) +# --- +# name: test_sensors[test_sensor_carbon_dioxide] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Test Sensor Carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '595.75', + }) +# --- +# name: test_sensors[test_sensor_humidity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Test Sensor Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.71', + }) +# --- +# name: test_sensors[test_sensor_pm10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Test Sensor PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.16', + }) +# --- +# name: test_sensors[test_sensor_pm2_5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Test Sensor PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.19', + }) +# --- +# name: test_sensors[test_sensor_temperature] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Sensor Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_sensor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.02', + }) +# --- +# name: test_sensors[test_sensor_total_volatile_organic_compounds] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Sensor Total volatile organic compounds', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.test_sensor_total_volatile_organic_compounds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- diff --git a/tests/components/arve/test_config_flow.py b/tests/components/arve/test_config_flow.py new file mode 100644 index 00000000000..efa36e37d44 --- /dev/null +++ b/tests/components/arve/test_config_flow.py @@ -0,0 +1,79 @@ +"""Test the Arve config flow.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.arve.config_flow import ArveConnectionError +from homeassistant.components.arve.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import USER_INPUT, async_init_integration + +from tests.common import MockConfigEntry + + +async def test_correct_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_arve: AsyncMock +) -> None: + """Test the whole flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == 12345 + + +async def test_form_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_arve: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + mock_arve.get_customer_id.side_effect = ArveConnectionError + 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, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_abort_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test form aborts if already configured.""" + await async_init_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ACCESS_TOKEN: "test-access-token", + CONF_CLIENT_SECRET: "test-customer-token", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/arve/test_sensor.py b/tests/components/arve/test_sensor.py new file mode 100644 index 00000000000..541820fd7b6 --- /dev/null +++ b/tests/components/arve/test_sensor.py @@ -0,0 +1,43 @@ +"""Test for Arve sensors.""" + +from unittest.mock import MagicMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_init_integration + +from tests.common import MockConfigEntry + +SENSORS = ( + "air_quality_index", + "carbon_dioxide", + "humidity", + "pm10", + "pm2_5", + "temperature", + "total_volatile_organic_compounds", +) + + +async def test_sensors( + hass: HomeAssistant, + mock_arve: MagicMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Arve sensors.""" + await async_init_integration(hass, mock_config_entry) + + for sensor in SENSORS: + state = hass.states.get(f"sensor.test_sensor_{sensor}") + assert state + assert state == snapshot(name=f"test_sensor_{sensor}") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot(name=f"entry_{sensor}") diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index 2566dfd61e6..4307e527cee 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -48,7 +48,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "aseko@example.com" assert result2["data"] == { CONF_EMAIL: "aseko@example.com", @@ -86,7 +86,7 @@ async def test_async_step_user_exception( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": reason} @@ -119,7 +119,7 @@ async def test_get_account_info_exceptions( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": reason} @@ -141,7 +141,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -200,5 +200,5 @@ async def test_async_step_reauth_exception( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": reason} diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index bbd0c9d333a..8124ed4ab85 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -34,7 +34,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }), @@ -123,7 +123,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en-US', }), @@ -212,7 +212,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en-US', }), @@ -325,7 +325,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }), diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 10a76bc9344..f952e3b7286 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -33,7 +33,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -114,7 +114,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -207,7 +207,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -409,7 +409,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -615,7 +615,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', }) @@ -637,7 +637,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', }) @@ -665,7 +665,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'never mind', 'language': 'en', }) @@ -799,7 +799,7 @@ dict({ 'conversation_id': 'mock-conversation-id', 'device_id': 'mock-device-id', - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', }) diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index c6f45044cb3..f9b91af3bf1 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -727,7 +727,7 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: event_callback=event_callback, ) - assert run_1 == run_1 + assert run_1 == run_1 # noqa: PLR0124 assert run_1 != run_2 assert run_1 != 1234 diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 3bfe6605839..cf3afff0172 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, patch import pytest +from homeassistant.components import conversation from homeassistant.components.assist_pipeline.const import DOMAIN from homeassistant.components.assist_pipeline.pipeline import ( STORAGE_KEY, @@ -18,6 +19,7 @@ from homeassistant.components.assist_pipeline.pipeline import ( async_create_default_pipeline, async_get_pipeline, async_get_pipelines, + async_migrate_engine, async_update_pipeline, ) from homeassistant.core import HomeAssistant @@ -117,6 +119,13 @@ async def test_loading_pipelines_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored pipelines on start.""" + async_migrate_engine( + hass, + "conversation", + conversation.OLD_HOME_ASSISTANT_AGENT, + conversation.HOME_ASSISTANT_AGENT, + ) + id_1 = "01GX8ZWBAQYWNB1XV3EXEZ75DY" hass_storage[STORAGE_KEY] = { "version": STORAGE_VERSION, "minor_version": STORAGE_VERSION_MINOR, @@ -124,9 +133,9 @@ async def test_loading_pipelines_from_storage( "data": { "items": [ { - "conversation_engine": "conversation_engine_1", + "conversation_engine": conversation.OLD_HOME_ASSISTANT_AGENT, "conversation_language": "language_1", - "id": "01GX8ZWBAQYWNB1XV3EXEZ75DY", + "id": id_1, "language": "language_1", "name": "name_1", "stt_engine": "stt_engine_1", @@ -166,7 +175,7 @@ async def test_loading_pipelines_from_storage( "wake_word_id": "wakeword_id_3", }, ], - "preferred_item": "01GX8ZWBAQYWNB1XV3EXEZ75DY", + "preferred_item": id_1, }, } @@ -175,7 +184,8 @@ async def test_loading_pipelines_from_storage( pipeline_data: PipelineData = hass.data[DOMAIN] store = pipeline_data.pipeline_store assert len(store.data) == 3 - assert store.async_get_preferred_item() == "01GX8ZWBAQYWNB1XV3EXEZ75DY" + assert store.async_get_preferred_item() == id_1 + assert store.data[id_1].conversation_engine == conversation.HOME_ASSISTANT_AGENT async def test_migrate_pipeline_store( @@ -262,7 +272,7 @@ async def test_create_default_pipeline( tts_engine_id="test", pipeline_name="Test pipeline", ) == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=ANY, language="en", @@ -304,7 +314,7 @@ async def test_get_pipelines(hass: HomeAssistant) -> None: pipelines = async_get_pipelines(hass) assert list(pipelines) == [ Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=ANY, language="en", @@ -351,7 +361,7 @@ async def test_default_pipeline_no_stt_tts( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language=conv_language, id=pipeline.id, language=pipeline_language, @@ -414,7 +424,7 @@ async def test_default_pipeline( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language=conv_language, id=pipeline.id, language=pipeline_language, @@ -445,7 +455,7 @@ async def test_default_pipeline_unsupported_stt_language( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=pipeline.id, language="en", @@ -476,7 +486,7 @@ async def test_default_pipeline_unsupported_tts_language( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=pipeline.id, language="en", @@ -502,7 +512,7 @@ async def test_update_pipeline( pipelines = list(pipelines) assert pipelines == [ Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=ANY, language="en", @@ -611,3 +621,41 @@ async def test_update_pipeline( "wake_word_entity": "wake_work.test_1", "wake_word_id": "wake_word_id_1", } + + +async def test_migrate_after_load( + hass: HomeAssistant, init_supporting_components +) -> None: + """Test migrating an engine after done loading.""" + assert await async_setup_component(hass, "assist_pipeline", {}) + + pipeline_data: PipelineData = hass.data[DOMAIN] + store = pipeline_data.pipeline_store + assert len(store.data) == 1 + + assert ( + await async_create_default_pipeline( + hass, + stt_engine_id="bla", + tts_engine_id="bla", + pipeline_name="Bla pipeline", + ) + is None + ) + pipeline = await async_create_default_pipeline( + hass, + stt_engine_id="test", + tts_engine_id="test", + pipeline_name="Test pipeline", + ) + assert pipeline is not None + + async_migrate_engine(hass, "stt", "test", "stt.test") + async_migrate_engine(hass, "tts", "test", "tts.test") + + await hass.async_block_till_done(wait_background_tasks=True) + + pipeline_updated = async_get_pipeline(hass, pipeline.id) + + assert pipeline_updated.stt_engine == "stt.test" + assert pipeline_updated.tts_engine == "tts.test" diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 0883046f3a1..e08dd9685ea 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -1166,7 +1166,7 @@ async def test_get_pipeline( msg = await client.receive_json() assert msg["success"] assert msg["result"] == { - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en", "id": ANY, "language": "en", @@ -1250,7 +1250,7 @@ async def test_list_pipelines( assert msg["result"] == { "pipelines": [ { - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en", "id": ANY, "language": "en", @@ -2012,7 +2012,7 @@ async def test_wake_word_cooldown_different_entities( await client_pipeline.send_json_auto_id( { "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en-US", "language": "en", "name": "pipeline_with_wake_word_1", @@ -2032,7 +2032,7 @@ async def test_wake_word_cooldown_different_entities( await client_pipeline.send_json_auto_id( { "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en-US", "language": "en", "name": "pipeline_with_wake_word_2", diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 08ab2ae6c98..14b70811cde 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import patch from pyasuswrt import AsusWrtError import pytest -from homeassistant import data_entry_flow from homeassistant.components.asuswrt.const import ( CONF_DNSMASQ, CONF_INTERFACE, @@ -32,6 +31,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ASUSWRT_BASE, HOST, ROUTER_MAC_ADDR @@ -90,7 +90,7 @@ async def test_user_legacy( flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.FlowResultType.FORM + assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" connect_legacy.return_value.async_get_nvram.return_value = unique_id @@ -101,7 +101,7 @@ async def test_user_legacy( ) await hass.async_block_till_done() - assert legacy_result["type"] == data_entry_flow.FlowResultType.FORM + assert legacy_result["type"] is FlowResultType.FORM assert legacy_result["step_id"] == "legacy" # complete configuration @@ -110,7 +110,7 @@ async def test_user_legacy( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == {**CONFIG_DATA_TELNET, CONF_MODE: MODE_AP} @@ -125,7 +125,7 @@ async def test_user_http( flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.FlowResultType.FORM + assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" connect_http.return_value.mac = unique_id @@ -136,7 +136,7 @@ async def test_user_http( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA_HTTP @@ -153,7 +153,7 @@ async def test_error_pwd_required(hass: HomeAssistant, config) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "pwd_required"} @@ -166,7 +166,7 @@ async def test_error_no_password_ssh(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "pwd_or_ssh"} @@ -182,7 +182,7 @@ async def test_error_invalid_ssh(hass: HomeAssistant, patch_is_file) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "ssh_not_file"} @@ -195,7 +195,7 @@ async def test_error_invalid_host(hass: HomeAssistant, patch_get_host) -> None: data=CONFIG_DATA_TELNET, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "invalid_host"} @@ -211,7 +211,7 @@ async def test_abort_if_not_unique_id_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONFIG_DATA_TELNET, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_unique_id" @@ -233,7 +233,7 @@ async def test_update_uniqueid_exist( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA_HTTP prev_entry = hass.config_entries.async_get_entry(existing_entry.entry_id) @@ -255,7 +255,7 @@ async def test_abort_invalid_unique_id(hass: HomeAssistant, connect_legacy) -> N context={"source": SOURCE_USER}, data=CONFIG_DATA_TELNET, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -285,7 +285,7 @@ async def test_on_connect_legacy_failed( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: error} @@ -314,7 +314,7 @@ async def test_on_connect_http_failed( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: error} @@ -331,7 +331,7 @@ async def test_options_flow_ap(hass: HomeAssistant, patch_setup_entry) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert CONF_REQUIRE_IP in result["data_schema"].schema @@ -346,7 +346,7 @@ async def test_options_flow_ap(hass: HomeAssistant, patch_setup_entry) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 20, CONF_TRACK_UNKNOWN: True, @@ -368,7 +368,7 @@ async def test_options_flow_router(hass: HomeAssistant, patch_setup_entry) -> No await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert CONF_REQUIRE_IP not in result["data_schema"].schema @@ -382,7 +382,7 @@ async def test_options_flow_router(hass: HomeAssistant, patch_setup_entry) -> No }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 20, CONF_TRACK_UNKNOWN: True, @@ -403,7 +403,7 @@ async def test_options_flow_http(hass: HomeAssistant, patch_setup_entry) -> None await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert CONF_INTERFACE not in result["data_schema"].schema assert CONF_DNSMASQ not in result["data_schema"].schema @@ -417,7 +417,7 @@ async def test_options_flow_http(hass: HomeAssistant, patch_setup_entry) -> None }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 20, CONF_TRACK_UNKNOWN: True, diff --git a/tests/components/asuswrt/test_diagnostics.py b/tests/components/asuswrt/test_diagnostics.py index 1c09dd29adc..207f3ba25f0 100644 --- a/tests/components/asuswrt/test_diagnostics.py +++ b/tests/components/asuswrt/test_diagnostics.py @@ -30,7 +30,7 @@ async def test_diagnostics( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED entry_dict = async_redact_data(mock_config_entry.as_dict(), TO_REDACT) diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index 138790e77e8..59dd7fe8b48 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import PropertyMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.atag import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import UID, USER_INPUT, init_integration, mock_connection @@ -24,7 +25,7 @@ async def test_show_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -38,7 +39,7 @@ async def test_adding_second_device( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" with patch( "pyatag.AtagOne.id", @@ -47,7 +48,7 @@ async def test_adding_second_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_connection_error( @@ -61,7 +62,7 @@ async def test_connection_error( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -76,7 +77,7 @@ async def test_unauthorized( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unauthorized"} @@ -91,6 +92,6 @@ async def test_full_flow_implementation( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == UID assert result["result"].unique_id == UID diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index d8f42e48842..a91c4eb8bc9 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import patch from aiohttp import ClientError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.aurora.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -41,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aurora visibility" assert result2["data"] == DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +64,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -84,7 +85,7 @@ async def test_with_unknown_error(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -104,7 +105,7 @@ async def test_option_flow(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -112,5 +113,5 @@ async def test_option_flow(hass: HomeAssistant) -> None: user_input={"forecast_threshold": 65}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["forecast_threshold"] == 65 diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index fbeaff2f4f8..9c27c14d633 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aurorapy.client import AuroraError, AuroraTimeoutError from serial.tools import list_ports_common -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.aurora_abb_powerone.const import ( ATTR_FIRMWARE, ATTR_MODEL, @@ -13,6 +13,7 @@ from homeassistant.components.aurora_abb_powerone.const import ( ) from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} @@ -30,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -64,7 +65,7 @@ async def test_form(hass: HomeAssistant) -> None: {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_PORT: "/dev/ttyUSB7", @@ -90,7 +91,7 @@ async def test_form_no_comports(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_serial_ports" @@ -106,7 +107,7 @@ async def test_form_invalid_com_ports(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index f08b56502b8..6ee674ab0f4 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["errors"] is None with ( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -91,7 +91,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -100,7 +100,7 @@ async def test_no_services(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["errors"] is None with ( @@ -118,7 +118,7 @@ async def test_no_services(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_services_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -138,7 +138,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -157,7 +157,7 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -188,7 +188,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA @@ -233,5 +233,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result7["type"] == "abort" + assert result7["type"] is FlowResultType.ABORT assert result7["reason"] == "reauth_successful" diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 00f7e5e7a83..825c10e92ee 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -5,9 +5,9 @@ from unittest.mock import patch from aiohttp import ClientConnectionError from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType -from homeassistant import data_entry_flow from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import setup_platform @@ -25,7 +25,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: with patch( "homeassistant.components.aussie_broadband.config_flow.AussieBroadbandConfigFlow.async_step_reauth", return_value={ - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "flow_id": "mock_flow", "step_id": "reauth_confirm", }, diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index f8c3153fba6..cb8d0d81ffe 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -1,9 +1,9 @@ """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 from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded @@ -75,7 +75,8 @@ async def test_ws_setup_depose_mfa( assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.FlowResultType.FORM + # Cannot use identity `is` check here as the value is parsed from JSON + assert flow["type"] == FlowResultType.FORM.value assert flow["handler"] == "example_module" assert flow["step_id"] == "init" assert flow["data_schema"][0] == {"type": "string", "name": "pin", "required": True} @@ -94,7 +95,8 @@ async def test_ws_setup_depose_mfa( assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + # Cannot use identity `is` check here as the value is parsed from JSON + assert flow["type"] == FlowResultType.CREATE_ENTRY.value assert flow["handler"] == "example_module" assert flow["data"]["result"] is None diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 24d77800508..7e29c134462 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -8,9 +8,9 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components import automation from homeassistant.components.blueprint import models +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -48,7 +48,7 @@ async def test_notify_leaving_zone( ) -> None: """Test notifying leaving a zone blueprint.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index f6567285ab0..edf0eff878b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -8,8 +8,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries -import homeassistant.components.automation as automation +from homeassistant.components import automation, input_boolean, script from homeassistant.components.automation import ( ATTR_SOURCE, DOMAIN, @@ -18,9 +17,11 @@ from homeassistant.components.automation import ( SERVICE_TRIGGER, AutomationEntity, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + CONF_ID, EVENT_HOMEASSISTANT_STARTED, SERVICE_RELOAD, SERVICE_TOGGLE, @@ -40,6 +41,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.script import ( SCRIPT_MODE_CHOICES, SCRIPT_MODE_PARALLEL, @@ -692,7 +694,9 @@ async def test_reload_config_handles_load_fails(hass: HomeAssistant, calls) -> N assert len(calls) == 2 -@pytest.mark.parametrize("service", ["turn_off_stop", "turn_off_no_stop", "reload"]) +@pytest.mark.parametrize( + "service", ["turn_off_stop", "turn_off_no_stop", "reload", "reload_single"] +) async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: """Test that turning off / reloading stops any running actions as appropriate.""" entity_id = "automation.hello" @@ -700,6 +704,7 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: config = { automation.DOMAIN: { + "id": "sun", "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": [ @@ -737,7 +742,7 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: {ATTR_ENTITY_ID: entity_id, automation.CONF_STOP_ACTIONS: False}, blocking=True, ) - else: + elif service == "reload": config[automation.DOMAIN]["alias"] = "goodbye" with patch( "homeassistant.config.load_yaml_config_file", @@ -747,6 +752,19 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: await hass.services.async_call( automation.DOMAIN, SERVICE_RELOAD, blocking=True ) + else: # service == "reload_single" + config[automation.DOMAIN]["alias"] = "goodbye" + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) hass.states.async_set(test_entity, "goodbye") await hass.async_block_till_done() @@ -801,6 +819,238 @@ async def test_reload_unchanged_does_not_stop( assert len(calls) == 1 +async def test_reload_single_unchanged_does_not_stop( + hass: HomeAssistant, calls +) -> None: + """Test that reloading stops any running actions as appropriate.""" + test_entity = "test.entity" + + config = { + automation.DOMAIN: { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [ + {"event": "running"}, + {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, + {"service": "test.automation"}, + ], + } + } + assert await async_setup_component(hass, automation.DOMAIN, config) + + running = asyncio.Event() + + @callback + def running_cb(event): + running.set() + + hass.bus.async_listen_once("running", running_cb) + hass.states.async_set(test_entity, "hello") + + hass.bus.async_fire("test_event") + await running.wait() + assert len(calls) == 0 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) + + hass.states.async_set(test_entity, "goodbye") + await hass.async_block_till_done() + + assert len(calls) == 1 + + +async def test_reload_single_add_automation(hass: HomeAssistant, calls) -> None: + """Test that reloading a single automation.""" + config1 = {automation.DOMAIN: {}} + config2 = { + automation.DOMAIN: { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "test.automation"}], + } + } + assert await async_setup_component(hass, automation.DOMAIN, config1) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config2, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_reload_single_parallel_calls(hass: HomeAssistant, calls) -> None: + """Test reloading single automations in parallel.""" + config1 = {automation.DOMAIN: {}} + config2 = { + automation.DOMAIN: [ + { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event_sun"}, + "action": [{"service": "test.automation"}], + }, + { + "id": "moon", + "alias": "goodbye", + "trigger": {"platform": "event", "event_type": "test_event_moon"}, + "action": [{"service": "test.automation"}], + }, + { + "id": "mars", + "alias": "goodbye", + "trigger": {"platform": "event", "event_type": "test_event_mars"}, + "action": [{"service": "test.automation"}], + }, + { + "id": "venus", + "alias": "goodbye", + "trigger": {"platform": "event", "event_type": "test_event_venus"}, + "action": [{"service": "test.automation"}], + }, + ] + } + assert await async_setup_component(hass, automation.DOMAIN, config1) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Trigger multiple reload service calls, each automation is reloaded twice. + # This tests the logic in the `ReloadServiceHelper` which avoids redundant + # reloads of the same target automation. + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config2, + ): + tasks = [ + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "moon"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "mars"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "venus"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "moon"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "mars"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "venus"}, + blocking=False, + ), + ] + await asyncio.gather(*tasks) + await hass.async_block_till_done() + + # Sanity check to ensure all automations are correctly setup + hass.bus.async_fire("test_event_sun") + await hass.async_block_till_done() + assert len(calls) == 1 + hass.bus.async_fire("test_event_moon") + await hass.async_block_till_done() + assert len(calls) == 2 + hass.bus.async_fire("test_event_mars") + await hass.async_block_till_done() + assert len(calls) == 3 + hass.bus.async_fire("test_event_venus") + await hass.async_block_till_done() + assert len(calls) == 4 + + +async def test_reload_single_remove_automation(hass: HomeAssistant, calls) -> None: + """Test that reloading a single automation.""" + config1 = { + automation.DOMAIN: { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "test.automation"}], + } + } + config2 = {automation.DOMAIN: {}} + assert await async_setup_component(hass, automation.DOMAIN, config1) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config2, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_reload_moved_automation_without_alias( hass: HomeAssistant, calls ) -> None: @@ -1615,7 +1865,7 @@ async def test_extraction_functions( ) -> None: """Test extraction functions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) condition_device = device_registry.async_get_or_create( @@ -2506,6 +2756,7 @@ async def test_recursive_automation_starting_script( async def async_automation_triggered(event): """Listen to automation_triggered event from the automation integration.""" automation_triggered.append(event) + await asyncio.sleep(0) # Yield to allow other tasks to run hass.states.async_set("sensor.test", str(len(automation_triggered))) hass.services.async_register("test", "script_done", async_service_handler) @@ -2730,3 +2981,82 @@ async def test_automation_turns_off_other_automation( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() assert len(calls) == 0 + + +async def test_two_automations_call_restart_script_same_time( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test two automations that call a restart mode script at the same.""" + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + events = [] + + @callback + def _save_event(event): + events.append(event) + + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + { + input_boolean.DOMAIN: { + "test_1": None, + } + }, + ) + cancel = async_track_state_change_event(hass, "input_boolean.test_1", _save_event) + + assert await async_setup_component( + hass, + script.DOMAIN, + { + script.DOMAIN: { + "fire_toggle": { + "sequence": [ + { + "service": "input_boolean.toggle", + "target": {"entity_id": "input_boolean.test_1"}, + } + ] + }, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "to": "on", + }, + "action": { + "service": "script.fire_toggle", + }, + "id": "automation_0", + "mode": "single", + }, + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "to": "on", + }, + "action": { + "service": "script.fire_toggle", + }, + "id": "automation_1", + "mode": "single", + }, + ] + }, + ) + + hass.states.async_set("binary_sensor.presence", "on") + await hass.async_block_till_done() + assert len(events) == 2 + cancel() diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index c8b9ea262a8..ab9f5faa425 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -6,11 +6,11 @@ from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientConnectorError from python_awair.exceptions import AuthError, AwairError -from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( CLOUD_CONFIG, @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "user" @@ -72,7 +72,7 @@ async def test_unexpected_api_error(hass: HomeAssistant) -> None: CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -101,7 +101,7 @@ async def test_duplicate_error(hass: HomeAssistant, user, cloud_devices) -> None CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -123,7 +123,7 @@ async def test_no_devices_error(hass: HomeAssistant, user, no_devices) -> None: CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -151,7 +151,7 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: user_input=CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -167,7 +167,7 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: user_input=CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -185,7 +185,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -195,7 +195,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: user_input=CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -226,7 +226,7 @@ async def test_create_cloud_entry(hass: HomeAssistant, user, cloud_devices) -> N CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "foo@bar.com" assert result["data"][CONF_ACCESS_TOKEN] == CLOUD_CONFIG[CONF_ACCESS_TOKEN] assert result["result"].unique_id == CLOUD_UNIQUE_ID @@ -262,7 +262,7 @@ async def test_create_local_entry(hass: HomeAssistant, local_devices) -> None: LOCAL_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] assert result["result"].unique_id == LOCAL_UNIQUE_ID @@ -308,7 +308,7 @@ async def test_create_local_entry_from_discovery( {"device": LOCAL_CONFIG[CONF_HOST]}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] assert result["result"].unique_id == LOCAL_UNIQUE_ID @@ -342,7 +342,7 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant) -> None: ) # User is returned to form to try again - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "local_pick" @@ -365,7 +365,7 @@ async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices) -> None {}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host assert result["result"].unique_id == LOCAL_UNIQUE_ID @@ -382,7 +382,7 @@ async def test_unsuccessful_create_zeroconf_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zeroconf_discovery_update_configuration( @@ -414,7 +414,7 @@ async def test_zeroconf_discovery_update_configuration( data=ZEROCONF_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert config_entry.data[CONF_HOST] == ZEROCONF_DISCOVERY.host @@ -440,7 +440,7 @@ async def test_zeroconf_during_onboarding( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Awair Element (24947)" assert "data" in result assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index b50a28df49f..7a4e446a0cc 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -114,6 +114,7 @@ def default_request_fixture( port_management_payload: dict[str, Any], param_properties_payload: dict[str, Any], param_ports_payload: dict[str, Any], + mqtt_status_code: int, ) -> Callable[[str], None]: """Mock default Vapix requests responses.""" @@ -131,7 +132,7 @@ def default_request_fixture( json=port_management_payload, ) respx.post("/axis-cgi/mqtt/client.cgi").respond( - json=MQTT_CLIENT_RESPONSE, + json=MQTT_CLIENT_RESPONSE, status_code=mqtt_status_code ) respx.post("/axis-cgi/streamprofile.cgi").respond( json=STREAM_PROFILES_RESPONSE, @@ -239,6 +240,12 @@ def param_ports_data_fixture() -> dict[str, Any]: return PORTS_RESPONSE +@pytest.fixture(name="mqtt_status_code") +def mqtt_status_code_fixture(): + """Property parameter data.""" + return 200 + + @pytest.fixture(name="setup_default_vapix_requests") def default_vapix_requests_fixture(mock_vapix_requests: Callable[[str], None]) -> None: """Mock default Vapix requests responses.""" diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a6a0235b118..68dca3539c5 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -61,7 +61,7 @@ async def test_flow_manual_configuration( AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -75,7 +75,7 @@ async def test_flow_manual_configuration( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_PROTOCOL: "http", @@ -98,7 +98,7 @@ async def test_manual_configuration_update_configuration( AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_vapix_requests("2.3.4.5") @@ -114,7 +114,7 @@ async def test_manual_configuration_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" @@ -125,7 +125,7 @@ async def test_flow_fails_faulty_credentials(hass: HomeAssistant) -> None: AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -152,7 +152,7 @@ async def test_flow_fails_cannot_connect(hass: HomeAssistant) -> None: AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -192,7 +192,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -206,7 +206,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_PROTOCOL: "http", @@ -235,7 +235,7 @@ async def test_reauth_flow_update_configuration( data=mock_config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_vapix_requests("2.3.4.5") @@ -251,7 +251,7 @@ async def test_reauth_flow_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is 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" @@ -276,7 +276,7 @@ async def test_reconfiguration_flow_update_configuration( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_vapix_requests("2.3.4.5") @@ -289,7 +289,7 @@ async def test_reconfiguration_flow_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is 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" @@ -374,7 +374,7 @@ async def test_discovery_flow( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" flows = hass.config_entries.flow.async_progress() @@ -392,7 +392,7 @@ async def test_discovery_flow( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_PROTOCOL: "http", @@ -454,7 +454,7 @@ async def test_discovered_device_already_configured( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == DEFAULT_HOST @@ -523,7 +523,7 @@ async def test_discovery_flow_updated_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data == { CONF_HOST: "2.3.4.5", @@ -580,7 +580,7 @@ async def test_discovery_flow_ignore_non_axis_device( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_axis_device" @@ -629,7 +629,7 @@ async def test_discovery_flow_ignore_link_local_address( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -640,7 +640,7 @@ async def test_option_flow(hass: HomeAssistant, setup_config_entry) -> None: result = await hass.config_entries.options.async_init(setup_config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_stream" assert set(result["data_schema"].schema[CONF_STREAM_PROFILE].container) == { DEFAULT_STREAM_PROFILE, @@ -657,7 +657,7 @@ async def test_option_flow(hass: HomeAssistant, setup_config_entry) -> None: user_input={CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1, diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index 1ae6db05427..5948874f0bf 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -2,7 +2,7 @@ from ipaddress import ip_address from unittest import mock -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import axis as axislib import pytest @@ -91,7 +91,8 @@ 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"axis/{MAC}/#", mock.ANY, 0, "utf-8") + mqtt_call = call(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") + assert mqtt_call in mqtt_mock.async_subscribe.call_args_list topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" message = ( @@ -109,6 +110,16 @@ async def test_device_support_mqtt( assert pir.name == f"{NAME} PIR 0" +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) +@pytest.mark.parametrize("mqtt_status_code", [401]) +async def test_device_support_mqtt_low_privilege( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry +) -> None: + """Successful setup.""" + mqtt_call = call(f"{MAC}/#", mock.ANY, 0, "utf-8") + assert mqtt_call not in mqtt_mock.async_subscribe.call_args_list + + async def test_update_address( hass: HomeAssistant, setup_config_entry, mock_vapix_requests ) -> None: diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 7a22597197b..607508b985a 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant async def test_setup_entry(hass: HomeAssistant, setup_config_entry) -> None: """Test successful setup of entry.""" - assert setup_config_entry.state == ConfigEntryState.LOADED + assert setup_config_entry.state is ConfigEntryState.LOADED async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: @@ -24,15 +24,15 @@ async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass: HomeAssistant, setup_config_entry) -> None: """Test successful unload of entry.""" - assert setup_config_entry.state == ConfigEntryState.LOADED + assert setup_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(setup_config_entry.entry_id) - assert setup_config_entry.state == ConfigEntryState.NOT_LOADED + assert setup_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize("config_entry_version", [1]) @@ -49,5 +49,5 @@ async def test_migrate_entry(hass: HomeAssistant, config_entry) -> None: with patch("homeassistant.components.axis.async_setup_entry", return_value=True): assert await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.version == 3 diff --git a/tests/components/azure_devops/__init__.py b/tests/components/azure_devops/__init__.py index da15bc6723d..fb0817671b5 100644 --- a/tests/components/azure_devops/__init__.py +++ b/tests/components/azure_devops/__init__.py @@ -1 +1,82 @@ """Tests for the Azure DevOps integration.""" + +from typing import Final + +from aioazuredevops.builds import DevOpsBuild, DevOpsBuildDefinition +from aioazuredevops.core import DevOpsProject + +from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PAT, CONF_PROJECT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +ORGANIZATION: Final[str] = "testorg" +PROJECT: Final[str] = "testproject" +PAT: Final[str] = "abc123" + +UNIQUE_ID = f"{ORGANIZATION}_{PROJECT}" + + +FIXTURE_USER_INPUT = { + CONF_ORG: ORGANIZATION, + CONF_PROJECT: PROJECT, + CONF_PAT: PAT, +} + +FIXTURE_REAUTH_INPUT = { + CONF_PAT: PAT, +} + + +DEVOPS_PROJECT = DevOpsProject( + project_id="1234", + name=PROJECT, + description="Test Description", + url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}", + state="wellFormed", + revision=1, + visibility="private", + last_updated=None, + default_team=None, + links=None, +) + +DEVOPS_BUILD_DEFINITION = DevOpsBuildDefinition( + build_id=9876, + name="Test Build", + url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}/_apis/build/definitions/1", + path="", + build_type="build", + queue_status="enabled", + revision=1, +) + +DEVOPS_BUILD = DevOpsBuild( + build_id=5678, + build_number="1", + status="completed", + result="succeeded", + source_branch="main", + source_version="123", + priority="normal", + reason="manual", + queue_time="2021-01-01T00:00:00Z", + start_time="2021-01-01T00:00:00Z", + finish_time="2021-01-01T00:00:00Z", + definition=DEVOPS_BUILD_DEFINITION, + project=DEVOPS_PROJECT, + links=None, +) + + +async def setup_integration( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> bool: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return result diff --git a/tests/components/azure_devops/conftest.py b/tests/components/azure_devops/conftest.py new file mode 100644 index 00000000000..d51142cdced --- /dev/null +++ b/tests/components/azure_devops/conftest.py @@ -0,0 +1,58 @@ +"""Test fixtures for Azure DevOps.""" + +from collections.abc import AsyncGenerator, Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.azure_devops.const import DOMAIN + +from . import DEVOPS_BUILD, DEVOPS_PROJECT, FIXTURE_USER_INPUT, PAT, UNIQUE_ID + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def mock_devops_client() -> AsyncGenerator[MagicMock, None]: + """Mock the Azure DevOps client.""" + + with ( + patch( + "homeassistant.components.azure_devops.DevOpsClient", autospec=True + ) as mock_client, + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient", + new=mock_client, + ), + ): + devops_client = mock_client.return_value + devops_client.authorized = True + devops_client.pat = PAT + devops_client.authorize.return_value = True + devops_client.get_project.return_value = DEVOPS_PROJECT + devops_client.get_builds.return_value = [DEVOPS_BUILD] + devops_client.get_build.return_value = DEVOPS_BUILD + devops_client.get_work_items_ids_all.return_value = None + devops_client.get_work_items.return_value = None + + yield devops_client + + +@pytest.fixture +async def mock_config_entry() -> MockConfigEntry: + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=FIXTURE_USER_INPUT, + unique_id=UNIQUE_ID, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.azure_devops.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/azure_devops/snapshots/test_sensor.ambr b/tests/components/azure_devops/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..b99d2c4e49d --- /dev/null +++ b/tests/components/azure_devops/snapshots/test_sensor.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_sensors[sensor.testproject_test_build_latest_build-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.testproject_test_build_latest_build', + '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': 'Test Build latest build', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latest_build', + 'unique_id': 'testorg_1234_9876_latest_build', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_test_build_latest_build-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'definition_id': 9876, + 'definition_name': 'Test Build', + 'finish_time': '2021-01-01T00:00:00Z', + 'friendly_name': 'testproject Test Build latest build', + 'id': 5678, + 'queue_time': '2021-01-01T00:00:00Z', + 'reason': 'manual', + 'result': 'succeeded', + 'source_branch': 'main', + 'source_version': '123', + 'start_time': '2021-01-01T00:00:00Z', + 'status': 'completed', + 'url': None, + }), + 'context': , + 'entity_id': 'sensor.testproject_test_build_latest_build', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 84c2b5d3cca..acb610a78be 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -1,26 +1,19 @@ """Test the Azure DevOps config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock from aioazuredevops.core import DevOpsProject import aiohttp -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.azure_devops.const import ( - CONF_ORG, - CONF_PAT, - CONF_PROJECT, - DOMAIN, -) +from homeassistant import config_entries +from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PROJECT, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import FIXTURE_REAUTH_INPUT, FIXTURE_USER_INPUT from tests.common import MockConfigEntry -FIXTURE_REAUTH_INPUT = {CONF_PAT: "abc123"} -FIXTURE_USER_INPUT = {CONF_ORG: "random", CONF_PROJECT: "project", CONF_PAT: "abc123"} - -UNIQUE_ID = "random_project" - async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -28,263 +21,242 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" -async def test_authorization_error(hass: HomeAssistant) -> None: +async def test_authorization_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps authorization error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - return_value=False, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_authorization_error(hass: HomeAssistant) -> None: +async def test_reauth_authorization_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps authorization error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - return_value=False, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "invalid_auth"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "invalid_auth"} -async def test_connection_error(hass: HomeAssistant) -> None: +async def test_connection_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps connection error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - side_effect=aiohttp.ClientError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + mock_devops_client.authorize.side_effect = aiohttp.ClientError + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} -async def test_reauth_connection_error(hass: HomeAssistant) -> None: +async def test_reauth_connection_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps connection error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - side_effect=aiohttp.ClientError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_devops_client.authorize.side_effect = aiohttp.ClientError + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "cannot_connect"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "cannot_connect"} -async def test_project_error(hass: HomeAssistant) -> None: +async def test_project_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> 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, - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + mock_devops_client.authorize.return_value = True + mock_devops_client.authorized = True + mock_devops_client.get_project.return_value = None - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "project_error"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "project_error"} -async def test_reauth_project_error(hass: HomeAssistant) -> None: +async def test_reauth_project_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> 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, - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_devops_client.authorize.return_value = True + mock_devops_client.authorized = True + mock_devops_client.get_project.return_value = None - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" + mock_config_entry.add_to_hass(hass) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "project_error"} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "project_error"} -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: """Test reauth works.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - return_value=False, - ): - mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT - ) - mock_config.add_to_hass(hass) + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_config_entry.add_to_hass(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - 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( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "invalid_auth"} - assert result2["type"] == data_entry_flow.FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + mock_devops_client.authorize.return_value = True + mock_devops_client.authorized = True + mock_devops_client.get_project.return_value = DevOpsProject( + "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" -async def test_full_flow_implementation(hass: HomeAssistant) -> None: +async def test_full_flow_implementation( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_devops_client: AsyncMock, +) -> 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] - ), - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + 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"] is FlowResultType.FORM + assert result["step_id"] == "user" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert ( - result2["title"] - == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" - ) - assert result2["data"][CONF_ORG] == FIXTURE_USER_INPUT[CONF_ORG] - assert result2["data"][CONF_PROJECT] == FIXTURE_USER_INPUT[CONF_PROJECT] + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert ( + result2["title"] + == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" + ) + assert result2["data"][CONF_ORG] == FIXTURE_USER_INPUT[CONF_ORG] + assert result2["data"][CONF_PROJECT] == FIXTURE_USER_INPUT[CONF_PROJECT] diff --git a/tests/components/azure_devops/test_init.py b/tests/components/azure_devops/test_init.py new file mode 100644 index 00000000000..a35acb375ec --- /dev/null +++ b/tests/components/azure_devops/test_init.py @@ -0,0 +1,78 @@ +"""Tests for init of Azure DevOps.""" + +from unittest.mock import AsyncMock, MagicMock + +import aiohttp + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a successful setup entry.""" + assert await setup_integration(hass, mock_config_entry) + + assert mock_devops_client.authorized + assert mock_devops_client.authorize.call_count == 1 + assert mock_devops_client.get_builds.call_count == 2 + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_auth_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: + """Test a failed setup entry.""" + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False + + await setup_integration(hass, mock_config_entry) + + assert not mock_devops_client.authorized + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_update_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a failed update entry.""" + mock_devops_client.get_builds.side_effect = aiohttp.ClientError + + await setup_integration(hass, mock_config_entry) + + assert mock_devops_client.get_builds.call_count == 1 + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_no_builds( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a failed update entry.""" + mock_devops_client.get_builds.return_value = None + + await setup_integration(hass, mock_config_entry) + + assert mock_devops_client.get_builds.call_count == 1 + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/azure_devops/test_sensor.py b/tests/components/azure_devops/test_sensor.py new file mode 100644 index 00000000000..1c518d919c2 --- /dev/null +++ b/tests/components/azure_devops/test_sensor.py @@ -0,0 +1,33 @@ +"""Tests for init of Azure DevOps.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: + """Test the sensor entities.""" + assert await setup_integration(hass, mock_config_entry) + + assert ( + entry := entity_registry.async_get("sensor.testproject_test_build_latest_build") + ) + + assert entry == snapshot(name=f"{entry.entity_id}-entry") + + assert hass.states.get(entry.entity_id) == snapshot(name=f"{entry.entity_id}-state") diff --git a/tests/components/azure_event_hub/conftest.py b/tests/components/azure_event_hub/conftest.py index 622b11000d7..99bf054dbb1 100644 --- a/tests/components/azure_event_hub/conftest.py +++ b/tests/components/azure_event_hub/conftest.py @@ -52,7 +52,7 @@ async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_b assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_FILTER: filter_schema}} ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Clear the component_loaded event from the queue. async_fire_time_changed( @@ -70,7 +70,7 @@ async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_b @pytest.fixture(name="entry_with_one_event") async def mock_entry_with_one_event(hass, entry): """Use the entry and add a single test event to the queue.""" - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.states.async_set("sensor.test", STATE_ON) return entry diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 38454e46dd1..cedbc5b43d6 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock from azure.eventhub.exceptions import EventHubError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.azure_event_hub.const import ( CONF_MAX_DELAY, CONF_SEND_INTERVAL, @@ -15,6 +15,7 @@ from homeassistant.components.azure_event_hub.const import ( STEP_SAS, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( BASE_CONFIG_CS, @@ -55,20 +56,20 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], step1_config.copy(), ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == step_id result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], step2_config.copy(), ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-instance" assert result3["data"] == data_config mock_setup_entry.assert_called_once() @@ -84,7 +85,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: data=IMPORT_CONFIG.copy(), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-instance" options = { CONF_SEND_INTERVAL: import_config.pop(CONF_SEND_INTERVAL), @@ -114,7 +115,7 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: context={"source": source}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -135,7 +136,7 @@ async def test_connection_error_sas( context={"source": config_entries.SOURCE_USER}, data=BASE_CONFIG_SAS.copy(), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_get_eventhub_properties.side_effect = side_effect @@ -143,7 +144,7 @@ async def test_connection_error_sas( result["flow_id"], SAS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -164,7 +165,7 @@ async def test_connection_error_cs( context={"source": config_entries.SOURCE_USER}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_from_connection_string.return_value.get_eventhub_properties.side_effect = ( side_effect @@ -173,7 +174,7 @@ async def test_connection_error_cs( result["flow_id"], CS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -181,13 +182,13 @@ async def test_options_flow(hass: HomeAssistant, entry) -> None: """Test options flow.""" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], UPDATE_OPTIONS ) - assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert updated["type"] is FlowResultType.CREATE_ENTRY assert updated["data"] == UPDATE_OPTIONS await hass.async_block_till_done() diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 0d5cfff80e9..1440bc2ede9 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -69,7 +69,7 @@ async def test_unload_entry(hass: HomeAssistant, entry, mock_create_batch) -> No """ assert await hass.config_entries.async_unload(entry.entry_id) mock_create_batch.add.assert_not_called() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_failed_test_connection( @@ -85,7 +85,7 @@ async def test_failed_test_connection( entry.add_to_hass(hass) mock_get_eventhub_properties.side_effect = EventHubError("Test") await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_send_batch_error( diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py index 648f235349d..09288c4a874 100644 --- a/tests/components/baf/__init__.py +++ b/tests/components/baf/__init__.py @@ -29,7 +29,6 @@ class MockBAFDevice(Device): """Mock async_wait_available.""" if self._async_wait_available_side_effect: raise self._async_wait_available_side_effect - return def async_run(self): """Mock async_run.""" diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index c7c56179839..765801d22cf 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -46,7 +46,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -80,7 +80,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -100,7 +100,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -113,7 +113,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Fan" assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -138,7 +138,7 @@ async def test_zeroconf_updates_existing_ip(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1" @@ -158,7 +158,7 @@ async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" @@ -177,12 +177,12 @@ async def test_user_flow_is_not_blocked_by_discovery(hass: HomeAssistant) -> Non type="mock_type", ), ) - assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -198,7 +198,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/baf/test_init.py b/tests/components/baf/test_init.py index f2616fdd96d..9de2fc03ed0 100644 --- a/tests/components/baf/test_init.py +++ b/tests/components/baf/test_init.py @@ -37,7 +37,7 @@ async def test_config_entry_wrong_uuid( with _patch_device_init(DeviceUUIDMismatchError): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY assert ( "Unexpected device found at 127.0.0.1; expected 12340, found 1234" in caplog.text diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index fce022572c3..7f679773f93 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Generator from unittest.mock import AsyncMock, MagicMock, patch -from pybalboa.enums import HeatMode +from pybalboa.enums import HeatMode, LowHighRange import pytest from homeassistant.core import HomeAssistant @@ -60,5 +60,6 @@ def client_fixture() -> Generator[MagicMock, None, None]: client.heat_state = 2 client.lights = [] client.pumps = [] + client.temperature_range.state = LowHighRange.LOW yield client diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index 66bc47d23f0..afa170577df 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from pybalboa.exceptions import SpaConnectionError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client: MagicMock) -> No result["flow_id"], TEST_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -81,7 +81,7 @@ async def test_form_spa_not_configured(hass: HomeAssistant, client: MagicMock) - result["flow_id"], TEST_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -101,7 +101,7 @@ async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -113,7 +113,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -132,7 +132,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -146,7 +146,7 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -159,5 +159,5 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert dict(config_entry.options) == {CONF_SYNC_TIME: True} diff --git a/tests/components/balboa/test_init.py b/tests/components/balboa/test_init.py index 867339c56ef..ecbadac0c09 100644 --- a/tests/components/balboa/test_init.py +++ b/tests/components/balboa/test_init.py @@ -16,9 +16,9 @@ async def test_setup_entry( hass: HomeAssistant, client: MagicMock, integration: MockConfigEntry ) -> None: """Validate that setup entry also configure the client.""" - assert integration.state == ConfigEntryState.LOADED + assert integration.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(integration.entry_id) - assert integration.state == ConfigEntryState.NOT_LOADED + assert integration.state is ConfigEntryState.NOT_LOADED async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None: @@ -36,7 +36,7 @@ async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY client.connect.return_value = True client.async_configuration_loaded.return_value = False @@ -44,4 +44,4 @@ async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/balboa/test_select.py b/tests/components/balboa/test_select.py new file mode 100644 index 00000000000..bd79f024817 --- /dev/null +++ b/tests/components/balboa/test_select.py @@ -0,0 +1,85 @@ +"""Tests of the select entity of the balboa integration.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, call + +from pybalboa import SpaControl +from pybalboa.enums import LowHighRange +import pytest + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import client_update, init_integration + +ENTITY_SELECT = "select.fakespa_temperature_range" + + +@pytest.fixture +def mock_select(client: MagicMock): + """Return a mock switch.""" + select = MagicMock(SpaControl) + + async def set_state(state: LowHighRange): + select.state = state # mock the spacontrol state + + select.client = client + select.state = LowHighRange.LOW + select.set_state = set_state + client.temperature_range = select + return select + + +async def test_select(hass: HomeAssistant, client: MagicMock, mock_select) -> None: + """Test spa temperature range select.""" + await init_integration(hass) + + # check if the initial state is off + state = hass.states.get(ENTITY_SELECT) + assert state.state == LowHighRange.LOW.name.lower() + + # test high state + await _select_option_and_wait(hass, ENTITY_SELECT, LowHighRange.HIGH.name.lower()) + assert client.set_temperature_range.call_count == 1 + assert client.set_temperature_range.call_args == call(LowHighRange.HIGH) + + # test back to low state + await _select_option_and_wait(hass, ENTITY_SELECT, LowHighRange.LOW.name.lower()) + assert client.set_temperature_range.call_count == 2 # total call count + assert client.set_temperature_range.call_args == call(LowHighRange.LOW) + + +async def test_selected_option( + hass: HomeAssistant, client: MagicMock, mock_select +) -> None: + """Test spa temperature range selected option.""" + + await init_integration(hass) + + # ensure initial low state + state = hass.states.get(ENTITY_SELECT) + assert state.state == LowHighRange.LOW.name.lower() + + # ensure high state + mock_select.state = LowHighRange.HIGH + state = await client_update(hass, client, ENTITY_SELECT) + assert state.state == LowHighRange.HIGH.name.lower() + + +async def _select_option_and_wait(hass: HomeAssistant | None, entity, option): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity, + ATTR_OPTION: option, + }, + blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/bang_olufsen/test_config_flow.py b/tests/components/bang_olufsen/test_config_flow.py index d813ddf185b..ad513905f16 100644 --- a/tests/components/bang_olufsen/test_config_flow.py +++ b/tests/components/bang_olufsen/test_config_flow.py @@ -35,7 +35,7 @@ async def test_config_flow_timeout_error( context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "timeout_error"} assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -54,7 +54,7 @@ async def test_config_flow_client_connector_error( context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "client_connector_error"} assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -68,7 +68,7 @@ async def test_config_flow_invalid_ip(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER_INVALID, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "invalid_ip"} @@ -83,7 +83,7 @@ async def test_config_flow_api_exception( context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "api_exception"} assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -98,7 +98,7 @@ async def test_config_flow(hass: HomeAssistant, mock_mozart_client) -> None: data=None, ) - assert result_init["type"] == FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" result_user = await hass.config_entries.flow.async_configure( @@ -106,7 +106,7 @@ async def test_config_flow(hass: HomeAssistant, mock_mozart_client) -> None: user_input=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.CREATE_ENTRY + assert result_user["type"] is FlowResultType.CREATE_ENTRY assert result_user["data"] == TEST_DATA_CREATE_ENTRY assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -121,7 +121,7 @@ async def test_config_flow_zeroconf(hass: HomeAssistant, mock_mozart_client) -> data=TEST_DATA_ZEROCONF, ) - assert result_zeroconf["type"] == FlowResultType.FORM + assert result_zeroconf["type"] is FlowResultType.FORM assert result_zeroconf["step_id"] == "zeroconf_confirm" result_confirm = await hass.config_entries.flow.async_configure( @@ -129,7 +129,7 @@ async def test_config_flow_zeroconf(hass: HomeAssistant, mock_mozart_client) -> user_input=TEST_DATA_USER, ) - assert result_confirm["type"] == FlowResultType.CREATE_ENTRY + assert result_confirm["type"] is FlowResultType.CREATE_ENTRY assert result_confirm["data"] == TEST_DATA_CREATE_ENTRY assert mock_mozart_client.get_beolink_self.call_count == 0 @@ -144,7 +144,7 @@ async def test_config_flow_zeroconf_not_mozart_device(hass: HomeAssistant) -> No data=TEST_DATA_ZEROCONF_NOT_MOZART, ) - assert result_user["type"] == FlowResultType.ABORT + assert result_user["type"] is FlowResultType.ABORT assert result_user["reason"] == "not_mozart_device" @@ -157,5 +157,5 @@ async def test_config_flow_zeroconf_ipv6(hass: HomeAssistant) -> None: data=TEST_DATA_ZEROCONF_IPV6, ) - assert result_user["type"] == FlowResultType.ABORT + assert result_user["type"] is FlowResultType.ABORT assert result_user["reason"] == "ipv6_address" diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 2c94da10ce8..ac80878c836 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -651,7 +651,7 @@ async def test_probability_updates(hass: HomeAssistant) -> None: prob_given_false = [0.7, 0.4, 0.2] prior = 0.5 - for p_t, p_f in zip(prob_given_true, prob_given_false): + for p_t, p_f in zip(prob_given_true, prob_given_false, strict=False): prior = bayesian.update_probability(prior, p_t, p_f) assert round(abs(0.720000 - prior), 7) == 0 @@ -660,7 +660,7 @@ async def test_probability_updates(hass: HomeAssistant) -> None: prob_given_false = [0.6, 0.4, 0.2] prior = 0.7 - for p_t, p_f in zip(prob_given_true, prob_given_false): + for p_t, p_f in zip(prob_given_true, prob_given_false, strict=False): prior = bayesian.update_probability(prior, p_t, p_f) assert round(abs(0.9130434782608695 - prior), 7) == 0 diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 83451313bad..6837c882a01 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS from homeassistant.components.device_automation import DeviceAutomationType @@ -275,8 +275,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -294,8 +296,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -359,8 +363,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -421,9 +427,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index ad7bd9c3528..dd55682fc8d 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.components.device_automation import DeviceAutomationType @@ -277,15 +277,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -301,15 +298,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "not_bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "not_bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -379,15 +373,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -453,15 +444,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 3f59ed022fd..612c4f09424 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -6,9 +6,10 @@ from unittest.mock import DEFAULT, AsyncMock, PropertyMock, patch import blebox_uniapi import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.blebox import config_flow +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -66,7 +67,7 @@ async def test_flow_works( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -75,7 +76,7 @@ async def test_flow_works( data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My gate controller" assert result["data"] == { config_flow.CONF_HOST: "172.2.3.4", @@ -87,8 +88,7 @@ async def test_flow_works( def product_class_mock_fixture(): """Return a mocked feature.""" path = "homeassistant.components.blebox.config_flow.Box" - patcher = patch(path, DEFAULT, blebox_uniapi.box.Box, True, True) - return patcher + return patch(path, DEFAULT, blebox_uniapi.box.Box, True, True) async def test_flow_with_connection_failure( @@ -189,7 +189,7 @@ async def test_already_configured(hass: HomeAssistant, valid_feature_mock) -> No context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -203,7 +203,7 @@ async def test_async_setup_entry(hass: HomeAssistant, valid_feature_mock) -> Non await hass.async_block_till_done() assert hass.config_entries.async_entries() == [config] - assert config.state is config_entries.ConfigEntryState.LOADED + assert config.state is ConfigEntryState.LOADED async def test_async_remove_entry(hass: HomeAssistant, valid_feature_mock) -> None: @@ -219,7 +219,7 @@ async def test_async_remove_entry(hass: HomeAssistant, valid_feature_mock) -> No await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] - assert config.state is config_entries.ConfigEntryState.NOT_LOADED + assert config.state is ConfigEntryState.NOT_LOADED async def test_flow_with_zeroconf(hass: HomeAssistant) -> None: @@ -238,13 +238,13 @@ async def test_flow_with_zeroconf(hass: HomeAssistant) -> None: ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.blebox.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {"host": "172.100.123.4", "port": 80} @@ -278,7 +278,7 @@ async def test_flow_with_zeroconf_when_already_configured(hass: HomeAssistant) - ), ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -301,7 +301,7 @@ async def test_flow_with_zeroconf_when_device_unsupported(hass: HomeAssistant) - properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device_version" @@ -327,5 +327,5 @@ async def test_flow_with_zeroconf_when_device_response_unsupported( properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device_response" diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index b18fdf7615e..c6e3ee0960d 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -60,6 +60,7 @@ def blink_api_fixture(camera) -> MagicMock: mock_blink_api.refresh = AsyncMock(return_value=True) mock_blink_api.sync = MagicMock(return_value=True) mock_blink_api.cameras = {camera.name: camera} + mock_blink_api.request_homescreen = AsyncMock(return_value=True) with patch("homeassistant.components.blink.Blink") as class_mock: class_mock.return_value = mock_blink_api diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index b5fbf19ef9b..82ea847dcf2 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -5,9 +5,10 @@ from unittest.mock import patch from blinkpy.auth import LoginError from blinkpy.blinkpy import BlinkSetupError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.blink import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -16,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -36,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "blink" assert result2["result"].unique_id == "blink@example.com" assert result2["data"] == { @@ -71,7 +72,7 @@ async def test_form_2fa(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -97,7 +98,7 @@ async def test_form_2fa(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "blink" assert result3["result"].unique_id == "blink@example.com" assert len(mock_setup_entry.mock_calls) == 1 @@ -122,7 +123,7 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -148,7 +149,7 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: result2["flow_id"], {"pin": "1234"} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -171,7 +172,7 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -199,7 +200,7 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: result2["flow_id"], {"pin": "1234"} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "invalid_access_token"} @@ -222,7 +223,7 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -248,7 +249,7 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: result2["flow_id"], {"pin": "1234"} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "unknown"} @@ -266,7 +267,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], {"username": "blink@example.com", "password": "example"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -284,7 +285,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], {"username": "blink@example.com", "password": "example"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -295,5 +296,5 @@ async def test_reauth_shows_user_step(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH}, data={"username": "blink@example.com", "password": "invalid_password"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py index 1f3a4c956c4..46806ef3349 100644 --- a/tests/components/blink/test_init.py +++ b/tests/components/blink/test_init.py @@ -1,8 +1,9 @@ """Test the Blink init.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientError +from blinkpy.auth import LoginError import pytest from homeassistant.components.blink.const import ( @@ -53,9 +54,16 @@ async def test_setup_not_ready_authkey_required( """Test setup failed because 2FA is needed to connect to the Blink system.""" mock_blink_auth_api.check_key_required = MagicMock(return_value=True) + mock_blink_auth_api.send_auth_key = AsyncMock(return_value=False) mock_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + with patch( + "homeassistant.components.blink.config_flow.Auth.startup", + side_effect=LoginError, + ): + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/blue_current/test_config_flow.py b/tests/components/blue_current/test_config_flow.py index b5dad155618..33346990425 100644 --- a/tests/components/blue_current/test_config_flow.py +++ b/tests/components/blue_current/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["errors"] == {} - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_user(hass: HomeAssistant) -> None: @@ -34,7 +34,7 @@ async def test_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["errors"] == {} - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -60,7 +60,7 @@ async def test_user(hass: HomeAssistant) -> None: assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -85,7 +85,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - data={"api_token": "123"}, ) assert result["errors"]["base"] == message - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -111,7 +111,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -157,14 +157,14 @@ async def test_reauth( data={"api_token": "123"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_token": "1234567890"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert config_entry.data["api_token"] == expected_api_token diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index 06cc6b27c26..723dd993006 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -41,11 +41,11 @@ async def test_load_unload_entry( 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 is 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 + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( diff --git a/tests/components/bluemaestro/snapshots/test_sensor.ambr b/tests/components/bluemaestro/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..2b777ec6f09 --- /dev/null +++ b/tests/components/bluemaestro/snapshots/test_sensor.ambr @@ -0,0 +1,256 @@ +# serializer version: 1 +# name: test_sensors[sensor.tempo_disc_thd_eeff_battery-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.tempo_disc_thd_eeff_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Tempo Disc THD EEFF Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_dew_point-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.tempo_disc_thd_eeff_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point', + 'platform': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': 'aa:bb:cc:dd:ee:ff-dew_point', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tempo Disc THD EEFF Dew point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.1', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_humidity-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.tempo_disc_thd_eeff_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': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Tempo Disc THD EEFF Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49.8', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_signal_strength-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.tempo_disc_thd_eeff_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-signal_strength', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Tempo Disc THD EEFF Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-60', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_temperature-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.tempo_disc_thd_eeff_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': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tempo Disc THD EEFF Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.2', + }) +# --- diff --git a/tests/components/bluemaestro/test_config_flow.py b/tests/components/bluemaestro/test_config_flow.py index f87ea053ffe..819541c3b7f 100644 --- a/tests/components/bluemaestro/test_config_flow.py +++ b/tests/components/bluemaestro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.bluemaestro.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tempo Disc THD EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_bluemaestro(hass: HomeAssistant) -> None context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.bluemaestro.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tempo Disc THD EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.bluemaestro.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tempo Disc THD EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py index fdcb16730ff..a75e390c781 100644 --- a/tests/components/bluemaestro/test_sensor.py +++ b/tests/components/bluemaestro/test_sensor.py @@ -1,9 +1,11 @@ """Test the BlueMaestro sensors.""" +import pytest +from syrupy import SnapshotAssertion + 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 from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from . import BLUEMAESTRO_SERVICE_INFO @@ -11,7 +13,12 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -async def test_sensors(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: """Test setting up creates the sensors.""" entry = MockConfigEntry( domain=DOMAIN, @@ -25,14 +32,15 @@ async def test_sensors(hass: HomeAssistant) -> None: assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, BLUEMAESTRO_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 4 + assert len(hass.states.async_all("sensor")) == 5 + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - humid_sensor = hass.states.get("sensor.tempo_disc_thd_eeff_temperature") - humid_sensor_attrs = humid_sensor.attributes - assert humid_sensor.state == "24.2" - assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Tempo Disc THD EEFF Temperature" - assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" - assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + 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") assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index c1e040ccd49..d4056c1e38e 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -219,6 +219,45 @@ def two_adapters_fixture(): yield +@pytest.fixture(name="crashed_adapter") +def crashed_adapter_fixture(): + """Fixture that mocks one crashed 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:00", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": None, + "product": None, + "product_id": None, + "vendor_id": None, + }, + }, + ), + ): + yield + + @pytest.fixture(name="one_adapter_old_bluez") def one_adapter_old_bluez(): """Fixture that mocks two adapters on Linux.""" diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 9ca674e2d32..33474280ec4 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -32,6 +32,9 @@ async def test_options_flow_disabled_not_setup( domain=DOMAIN, data={}, options={}, unique_id=DEFAULT_ADDRESS ) entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + ws_client = await hass_ws_client(hass) await ws_client.send_json( @@ -53,7 +56,7 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "single_adapter" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -64,8 +67,8 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Core Bluetooth" + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Apple Unknown MacOS Model (Core Bluetooth)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -79,8 +82,13 @@ async def test_async_step_user_linux_one_adapter( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "single_adapter" + assert result["description_placeholders"] == { + "name": "hci0 (00:00:00:00:00:01)", + "model": "Bluetooth Adapter 5.0 (cc01:aa01)", + "manufacturer": "ACME", + } with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), patch( @@ -90,12 +98,25 @@ async def test_async_step_user_linux_one_adapter( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:01" + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 +async def test_async_step_user_linux_crashed_adapter( + hass: HomeAssistant, crashed_adapter: None +) -> None: + """Test setting up manually with one crashed adapter on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_adapters" + + async def test_async_step_user_linux_two_adapters( hass: HomeAssistant, two_adapters: None ) -> None: @@ -105,8 +126,12 @@ async def test_async_step_user_linux_two_adapters( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "multiple_adapters" + assert result["data_schema"].schema["adapter"].container == { + "hci0": "hci0 (00:00:00:00:00:01) ACME Bluetooth Adapter 5.0 (cc01:aa01)", + "hci1": "hci1 (00:00:00:00:00:02) ACME Bluetooth Adapter 5.0 (cc01:aa01)", + } with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), patch( @@ -116,8 +141,8 @@ async def test_async_step_user_linux_two_adapters( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADAPTER: "hci1"} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:02" + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:02)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -133,7 +158,7 @@ async def test_async_step_user_only_allows_one( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_adapters" @@ -152,7 +177,12 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM + assert result["description_placeholders"] == { + "name": "hci0 (00:00:00:00:00:01)", + "model": "Unknown", + "manufacturer": "ACME", + } assert result["step_id"] == "single_adapter" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -163,8 +193,8 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:01" + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "ACME Unknown (00:00:00:00:00:01)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -195,8 +225,8 @@ async def test_async_step_integration_discovery_during_onboarding_one_adapter( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "00:00:00:00:00:01" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "ACME Unknown (00:00:00:00:00:01)" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 @@ -239,12 +269,12 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci1", CONF_DETAILS: details2}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "00:00:00:00:00:01" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "ACME Unknown (00:00:00:00:00:01)" assert result["data"] == {} - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:02" + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "ACME Unknown (00:00:00:00:00:02)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 2 @@ -277,8 +307,8 @@ async def test_async_step_integration_discovery_during_onboarding( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "Core Bluetooth", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Core Bluetooth" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "ACME Unknown (Core Bluetooth)" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 @@ -302,7 +332,7 @@ async def test_async_step_integration_discovery_already_exists( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -325,7 +355,7 @@ async def test_options_flow_linux( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -337,13 +367,13 @@ async def test_options_flow_linux( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PASSIVE] is True # Verify we can change it to False result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -355,7 +385,7 @@ async def test_options_flow_linux( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PASSIVE] is False await hass.config_entries.async_unload(entry.entry_id) @@ -438,6 +468,6 @@ async def test_async_step_user_linux_adapter_is_ignored( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_adapters" assert result["description_placeholders"] == {"ignored_adapters": "1"} diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 3d29080d56c..c67bd583b1e 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -176,6 +176,14 @@ async def test_diagnostics( "source": "00:00:00:00:00:01", "start_time": ANY, "type": "HaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, }, { "adapter": "hci1", @@ -203,6 +211,14 @@ async def test_diagnostics( "source": "00:00:00:00:00:02", "start_time": ANY, "type": "FakeHaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, }, ], "slot_manager": { @@ -376,6 +392,14 @@ async def test_diagnostics_macos( "source": "Core Bluetooth", "start_time": ANY, "type": "FakeHaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, } ], "slot_manager": { @@ -543,6 +567,14 @@ async def test_diagnostics_remote_adapter( "source": "00:00:00:00:00:01", "start_time": ANY, "type": "HaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, }, { "connectable": True, diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e9198362d8f..8c26745d541 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -288,14 +288,14 @@ async def test_setup_and_retry_adapter_not_yet_available( assert "Failed to start Bluetooth" in caplog.text assert len(bluetooth.async_discovered_service_info(hass)) == 0 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with patch( "habluetooth.scanner.OriginalBleakScanner.start", ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "habluetooth.scanner.OriginalBleakScanner.stop", @@ -327,7 +327,7 @@ async def test_no_race_during_manual_reload_in_retry_state( assert "Failed to start Bluetooth" in caplog.text assert len(bluetooth.async_discovered_service_info(hass)) == 0 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with patch( "habluetooth.scanner.OriginalBleakScanner.start", @@ -335,7 +335,7 @@ async def test_no_race_during_manual_reload_in_retry_state( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "habluetooth.scanner.OriginalBleakScanner.stop", @@ -2807,6 +2807,19 @@ async def test_can_unsetup_bluetooth_single_adapter_macos( await hass.async_block_till_done() +async def test_default_address_config_entries_removed_linux( + hass: HomeAssistant, + mock_bleak_scanner_start: MagicMock, + one_adapter: None, +) -> None: + """Test default address entries are removed on linux.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) + entry.add_to_hass(hass) + await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + + async def test_can_unsetup_bluetooth_single_adapter_linux( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, @@ -2866,7 +2879,7 @@ async def test_three_adapters_one_missing( entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_auto_detect_bluetooth_adapters_linux( @@ -2889,6 +2902,16 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple( assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 +async def test_auto_detect_bluetooth_adapters_skips_crashed( + hass: HomeAssistant, crashed_adapter: None +) -> None: + """Test we skip crashed adapters on linux.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + async def test_auto_detect_bluetooth_adapters_linux_none_found( hass: HomeAssistant, ) -> None: @@ -3015,12 +3038,14 @@ async def test_discover_new_usb_adapters( "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, }, ), @@ -3088,12 +3113,14 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, }, ), @@ -3146,3 +3173,16 @@ async def test_haos_9_or_later( registry = async_get_issue_registry(hass) issue = registry.async_get_issue(DOMAIN, "haos_outdated") assert issue is None + + +async def test_title_updated_if_mac_address( + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None +) -> None: + """Test the title is updated if it is the mac address.""" + entry = MockConfigEntry( + domain="bluetooth", title="00:00:00:00:00:01", unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.title == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)" diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 504122fb671..5658aea523b 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -22,7 +22,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import ( - _get_manager, async_setup_with_one_adapter, generate_advertisement_data, generate_ble_device, @@ -48,7 +47,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( ) -> None: """Test we can reload if stopping the scanner raises.""" entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "habluetooth.scanner.OriginalBleakScanner.stop", @@ -57,7 +56,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert "Error stopping scanner" in caplog.text @@ -183,7 +182,7 @@ async def test_adapter_needs_reset_at_start( with ( patch( "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=[BleakError(error), None], + side_effect=[BleakError(error), BleakError(error), None], ), patch( "habluetooth.util.recover_adapter", return_value=True @@ -239,46 +238,47 @@ async def test_recovery_from_dbus_restart( assert called_start == 1 - start_time_monotonic = time.monotonic() - mock_discovered = [MagicMock()] + start_time_monotonic = time.monotonic() + mock_discovered = [MagicMock()] - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # Fire a callback to reset the timer - with patch_bluetooth_time( - start_time_monotonic, - ): - _callback( - generate_ble_device("44:44:33:11:23:42", "any_name"), - generate_advertisement_data(local_name="any_name"), - ) + # Fire a callback to reset the timer + with patch_bluetooth_time( + start_time_monotonic, + ): + _callback( + generate_ble_device("44:44:33:11:23:42", "any_name"), + generate_advertisement_data(local_name="any_name"), + ) - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # We hit the timer, so we restart the scanner - with patch_bluetooth_time( - start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT + 20, - ): - async_fire_time_changed( - hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL + timedelta(seconds=20) - ) - await hass.async_block_till_done() + # We hit the timer, so we restart the scanner + with patch_bluetooth_time( + start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT + 20, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL + timedelta(seconds=20), + ) + await hass.async_block_till_done() - assert called_start == 2 + assert called_start == 2 async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: @@ -327,43 +327,42 @@ async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: assert called_start == 1 - scanner = _get_manager() - mock_discovered = [MagicMock()] + mock_discovered = [MagicMock()] - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + 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, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # 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, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert len(mock_recover_adapter.mock_calls) == 1 - assert called_start == 2 + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 2 async def test_adapter_scanner_fails_to_start_first_time( @@ -418,61 +417,61 @@ async def test_adapter_scanner_fails_to_start_first_time( assert called_start == 1 - scanner = _get_manager() - mock_discovered = [MagicMock()] + mock_discovered = [MagicMock()] - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + 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, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # 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, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert len(mock_recover_adapter.mock_calls) == 1 - assert called_start == 3 + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 4 - # 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, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + now_monotonic = time.monotonic() + # We hit the timer again the previous start call failed, make sure + # we try again + with ( + patch_bluetooth_time( + now_monotonic + + SCANNER_WATCHDOG_TIMEOUT * 2 + + 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() - assert len(mock_recover_adapter.mock_calls) == 1 - assert called_start == 4 + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 5 async def test_adapter_fails_to_start_and_takes_a_bit_to_init( @@ -497,9 +496,11 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( nonlocal called_start called_start += 1 if called_start == 1: - raise BleakError("org.bluez.Error.InProgress") - if called_start == 2: raise BleakError("org.freedesktop.DBus.Error.UnknownObject") + if called_start == 2: + raise BleakError("org.bluez.Error.InProgress") + if called_start == 3: + raise BleakError("org.bluez.Error.InProgress") async def stop(self, *args, **kwargs): """Mock Start.""" @@ -538,7 +539,7 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( ): await async_setup_with_one_adapter(hass) - assert called_start == 3 + assert called_start == 4 assert len(mock_recover_adapter.mock_calls) == 1 assert "Waiting for adapter to initialize" in caplog.text diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 0630d671038..2acc2b0ddfc 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -107,7 +107,7 @@ class FakeBleakClientRaisesOnConnect(BaseFakeBleakClient): async def connect(self, *args, **kwargs): """Connect.""" - raise Exception("Test exception") + raise ConnectionError("Test exception") def _generate_ble_device_and_adv_data( @@ -304,8 +304,9 @@ async def test_release_slot_on_connect_exception( ): ble_device = hci0_device_advs["00:00:00:00:00:01"][0] client = bleak.BleakClient(ble_device) - with pytest.raises(Exception): - assert await client.connect() is False + with pytest.raises(ConnectionError) as exc_info: + await client.connect() + assert str(exc_info.value) == "Test exception" assert allocate_slot_mock.call_count == 1 assert release_slot_mock.call_count == 1 @@ -383,7 +384,7 @@ async def test_passing_subclassed_str_as_address( _, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices(hass) class SubclassedStr(str): - pass + __slots__ = () address = SubclassedStr("00:00:00:00:00:01") client = bleak.BleakClient(address) diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 84384e6b482..e737fce6897 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -54,9 +54,9 @@ async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: def check_remote_service_call( router: respx.MockRouter, - remote_service: str = None, - remote_service_params: dict = None, - remote_service_payload: dict = None, + remote_service: str | None = None, + remote_service_params: dict | None = None, + remote_service_payload: dict | None = None, ): """Check if the last call was a successful remote service call.""" diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index c3a89e28bd6..f43a7c089c7 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator -from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_STATES +from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_PROFILES, ALL_STATES from bimmer_connected.tests.common import MyBMWMockRouter from bimmer_connected.vehicle import remote_services import pytest @@ -23,6 +23,7 @@ def bmw_fixture( "WBA00000000DEMO03", "WBY00000000REXI01", ], + profiles=ALL_PROFILES, states=ALL_STATES, charging_settings=ALL_CHARGING_SETTINGS, ) diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index b3af5bc59b6..351c0f062fd 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -140,19 +140,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'INACTIVE', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -206,9 +195,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'BLUETOOTH', 'bodyType': 'I20', 'brand': 'BMW_I', 'color': 4285537312, @@ -223,7 +210,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'iX xDrive50', 'softwareVersionCurrent': dict({ 'iStep': 300, @@ -413,14 +399,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -786,19 +764,8 @@ 'is_pre_entry_climatization_enabled', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': 'CHARGING', 'charging_target': 80, 'is_charger_connected': True, @@ -829,6 +796,7 @@ 'software_version': '07/2021.00', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': True, 'is_remote_charge_stop_enabled': True, @@ -1026,19 +994,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'HEATING', 'activity_end_time': '2022-07-10T11:29:50+00:00', - 'activity_end_time_no_tz': '2022-07-10T11:29:50', 'is_climate_on': True, }), 'condition_based_services': dict({ @@ -1092,9 +1049,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', 'bodyType': 'G26', 'brand': 'BMW', 'color': 4284245350, @@ -1109,7 +1064,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'i4 eDrive40', 'softwareVersionCurrent': dict({ 'iStep': 470, @@ -1287,14 +1241,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -1651,19 +1597,8 @@ 'is_pre_entry_climatization_enabled', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': 'NOT_CHARGING', 'charging_target': 80, 'is_charger_connected': False, @@ -1694,6 +1629,7 @@ 'software_version': '11/2021.70', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -1770,23 +1706,7 @@ 'windows', ]), 'brand': 'bmw', - 'charging_profile': dict({ - 'ac_available_limits': None, - 'ac_current_limit': None, - 'charging_mode': 'IMMEDIATE_CHARGING', - 'charging_preferences': 'NO_PRESELECTION', - 'charging_preferences_service_pack': None, - 'departure_times': list([ - ]), - 'is_pre_entry_climatization_enabled': False, - 'preferred_charging_window': dict({ - '_window_dict': dict({ - }), - 'end_time': '00:00:00', - 'start_time': '00:00:00', - }), - 'timer_type': 'UNKNOWN', - }), + 'charging_profile': None, 'check_control_messages': dict({ 'has_check_control_messages': False, 'messages': list([ @@ -1803,19 +1723,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'INACTIVE', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -1878,9 +1787,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', 'bodyType': 'G20', 'brand': 'BMW', 'color': 4280233344, @@ -1895,7 +1802,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'M340i xDrive', 'softwareVersionCurrent': dict({ 'iStep': 470, @@ -1974,17 +1880,8 @@ 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', }), - 'charging_settings': dict({ - }), + 'charging_settings': None, 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingMode': 'IMMEDIATE_CHARGING', @@ -2288,19 +2185,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': None, 'charging_target': None, 'is_charger_connected': False, @@ -2331,6 +2217,7 @@ 'software_version': '07/2021.70', }), 'is_charging_plan_supported': False, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -2540,19 +2427,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'UNKNOWN', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -2588,9 +2464,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'CONNECTED', 'attributes': dict({ - 'a4aType': 'USB_ONLY', 'bodyType': 'I01', 'brand': 'BMW_I', 'color': 4284110934, @@ -2602,9 +2476,9 @@ 'iosAppScheme': 'bmwdriversguide:///open', 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', }), + 'headUnitRaw': 'MGU_02_L', 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -2622,6 +2496,7 @@ }), 'seriesCluster': 'I001', }), + 'telematicsUnit': 'WAVE01', 'year': 2015, }), 'capabilities': dict({ @@ -2731,10 +2606,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -2989,19 +2860,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': '2022-07-10T18:01:00+00:00', - 'charging_start_time_no_tz': '2022-07-10T18:01:00', 'charging_status': 'WAITING_FOR_CHARGING', 'charging_target': 100, 'is_charger_connected': True, @@ -3032,6 +2892,7 @@ 'software_version': '11/2021.10', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -3066,204 +2927,297 @@ ]), 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ @@ -4875,19 +4829,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'UNKNOWN', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -4923,9 +4866,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'CONNECTED', 'attributes': dict({ - 'a4aType': 'USB_ONLY', 'bodyType': 'I01', 'brand': 'BMW_I', 'color': 4284110934, @@ -4937,9 +4878,9 @@ 'iosAppScheme': 'bmwdriversguide:///open', 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', }), + 'headUnitRaw': 'MGU_02_L', 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -4957,6 +4898,7 @@ }), 'seriesCluster': 'I001', }), + 'telematicsUnit': 'WAVE01', 'year': 2015, }), 'capabilities': dict({ @@ -5066,10 +5008,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -5324,19 +5262,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': '2022-07-10T18:01:00+00:00', - 'charging_start_time_no_tz': '2022-07-10T18:01:00', 'charging_status': 'WAITING_FOR_CHARGING', 'charging_target': 100, 'is_charger_connected': True, @@ -5367,6 +5294,7 @@ 'software_version': '11/2021.10', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -5400,204 +5328,297 @@ }), 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ @@ -7062,204 +7083,297 @@ 'data': None, 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index e28b4485af0..dcf68622fdc 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -1,34 +1,6 @@ # serializer version: 1 # name: test_entity_state_attrs list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '340', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -54,6 +26,19 @@ 'last_updated': , 'state': 'CHARGING', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -69,6 +54,34 @@ 'last_updated': , 'state': '70', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '340', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -86,43 +99,21 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Charging target', - 'unit_of_measurement': '%', + 'device_class': 'enum', + 'friendly_name': 'iX xDrive50 Climate status', + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), }), 'context': , - 'entity_id': 'sensor.ix_xdrive50_charging_target', + 'entity_id': 'sensor.ix_xdrive50_climate_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '472', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', + 'state': 'inactive', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -149,6 +140,19 @@ 'last_updated': , 'state': 'NOT_CHARGING', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -164,6 +168,34 @@ 'last_updated': , 'state': '80', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '472', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -181,29 +213,21 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging target', - 'unit_of_measurement': '%', + 'device_class': 'enum', + 'friendly_name': 'i4 eDrive40 Climate status', + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), }), 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_target', + 'entity_id': 'sensor.i4_edrive40_climate_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '629', + 'state': 'heating', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -222,16 +246,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining fuel', + 'friendly_name': 'M340i xDrive Remaining range total', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_fuel', + 'entity_id': 'sensor.m340i_xdrive_remaining_range_total', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40', + 'state': '629', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -247,6 +271,20 @@ 'last_updated': , 'state': '629', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Remaining fuel', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_remaining_fuel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -264,30 +302,21 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range total', - 'state_class': , - 'unit_of_measurement': , + 'device_class': 'enum', + 'friendly_name': 'M340i xDrive Climate status', + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), }), 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_total', + 'entity_id': 'sensor.m340i_xdrive_climate_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '279', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '137009', + 'state': 'inactive', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -314,6 +343,19 @@ 'last_updated': , 'state': 'WAITING_FOR_CHARGING', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -329,6 +371,34 @@ 'last_updated': , 'state': '82', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '137009', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '279', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -346,15 +416,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Charging target', - 'unit_of_measurement': '%', + 'friendly_name': 'i3 (+ REX) Remaining range fuel', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.i3_rex_charging_target', + 'entity_id': 'sensor.i3_rex_remaining_range_fuel', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100', + 'state': '105', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -370,20 +441,6 @@ 'last_updated': , 'state': '6', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '105', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index ab7366e9da4..b562e2b898f 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -7,7 +7,7 @@ from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError from httpx import RequestError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN from homeassistant.components.bmw_connected_drive.const import ( CONF_READ_ONLY, @@ -15,6 +15,7 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( FIXTURE_CONFIG_ENTRY, @@ -41,7 +42,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -58,7 +59,7 @@ async def test_authentication_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -76,7 +77,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -94,7 +95,7 @@ async def test_api_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +118,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY @@ -143,7 +144,7 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "account_options" result = await hass.config_entries.options.async_configure( @@ -152,7 +153,7 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: True, } @@ -195,7 +196,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -204,7 +205,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data == FIXTURE_COMPLETE_ENTRY diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 619aa03572a..0aff18e6ed1 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -83,11 +83,11 @@ async def setup_platform( discovered_device: dict[str, Any], *, bond_device_id: str = "bond-device-id", - bond_version: dict[str, Any] = None, - props: dict[str, Any] = None, - state: dict[str, Any] = None, - bridge: dict[str, Any] = None, - token: dict[str, Any] = None, + bond_version: dict[str, Any] | None = None, + props: dict[str, Any] | None = None, + state: dict[str, Any] | None = None, + bridge: dict[str, Any] | None = None, + token: dict[str, Any] | None = None, ): """Set up the specified Bond platform.""" mock_entry = MockConfigEntry( diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index bfe61c536d9..d61ed4844a1 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.bond.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ( patch_bond_bridge, @@ -35,7 +36,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -53,7 +54,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "some host", @@ -68,7 +69,7 @@ async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -90,7 +91,7 @@ async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "New Fan" assert result2["data"] == { CONF_HOST: "some host", @@ -117,7 +118,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant) -> None: {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -137,7 +138,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -157,7 +158,7 @@ async def test_user_form_old_firmware(hass: HomeAssistant) -> None: {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "old_firmware"} @@ -205,7 +206,7 @@ async def test_user_form_one_entry_per_device_allowed(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -228,7 +229,7 @@ async def test_zeroconf_form(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -243,7 +244,7 @@ async def test_zeroconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -270,7 +271,7 @@ async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -285,7 +286,7 @@ async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -312,7 +313,7 @@ async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -327,7 +328,7 @@ async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -359,7 +360,7 @@ async def test_zeroconf_form_with_token_available(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup_entry() as mock_setup_entry: @@ -369,7 +370,7 @@ async def test_zeroconf_form_with_token_available(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "discovered-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -403,7 +404,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup_entry() as mock_setup_entry: @@ -413,7 +414,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ZXXX12345" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -448,7 +449,7 @@ async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "127.0.0.2" assert len(mock_setup_entry.mock_calls) == 1 @@ -486,7 +487,7 @@ async def test_zeroconf_in_setup_retry_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "127.0.0.2" assert len(mock_setup_entry.mock_calls) == 1 @@ -533,7 +534,7 @@ async def test_zeroconf_already_configured_refresh_token(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "127.0.0.2" assert entry.data[CONF_ACCESS_TOKEN] == "discovered-token" @@ -572,7 +573,7 @@ async def test_zeroconf_already_configured_no_reload_same_host( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -618,7 +619,7 @@ async def _help_test_form_unexpected_error( result["flow_id"], user_input ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 2fe2b98308d..b3a28151c93 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components import zeroconf from homeassistant.components.bosch_shc.config_flow import write_tls_asset from homeassistant.components.bosch_shc.const import CONF_SHC_CERT, CONF_SHC_KEY, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -40,7 +41,7 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -65,7 +66,7 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -92,7 +93,7 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "shc012345" assert result3["data"] == { "host": "1.1.1.1", @@ -125,7 +126,7 @@ async def test_form_get_info_connection_error( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -147,7 +148,7 @@ async def test_form_get_info_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -179,7 +180,7 @@ async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> N {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -193,7 +194,7 @@ async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> N ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "pairing_failed"} @@ -225,7 +226,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -251,7 +252,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "invalid_auth"} @@ -285,7 +286,7 @@ async def test_form_validate_connection_error( {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -311,7 +312,7 @@ async def test_form_validate_connection_error( ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "cannot_connect"} @@ -345,7 +346,7 @@ async def test_form_validate_session_error( {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -371,7 +372,7 @@ async def test_form_validate_session_error( ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "session_error"} @@ -405,7 +406,7 @@ async def test_form_validate_exception( {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -431,7 +432,7 @@ async def test_form_validate_exception( ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "unknown"} @@ -471,7 +472,7 @@ async def test_form_already_configured( {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -502,7 +503,7 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" assert result["errors"] == {} context = next( @@ -516,7 +517,7 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: result["flow_id"], {}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" with ( @@ -544,7 +545,7 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "shc012345" assert result3["data"] == { "host": "1.1.1.1", @@ -588,7 +589,7 @@ async def test_zeroconf_already_configured( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -607,7 +608,7 @@ async def test_zeroconf_cannot_connect( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -626,7 +627,7 @@ async def test_zeroconf_not_bosch_shc(hass: HomeAssistant, mock_zeroconf: None) ), context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_bosch_shc" @@ -650,7 +651,7 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: context={"source": config_entries.SOURCE_REAUTH}, data=mock_config.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -674,7 +675,7 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: {"host": "2.2.2.2"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -701,7 +702,7 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_config.data["host"] == "2.2.2.2" diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 673344017f7..6fc02dbd36f 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -10,7 +10,6 @@ from pybravia import ( ) import pytest -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.braviatv.const import ( CONF_NICKNAME, @@ -21,6 +20,7 @@ from homeassistant.components.braviatv.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import instance_id from tests.common import MockConfigEntry @@ -93,7 +93,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -105,7 +105,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=BRAVIA_SSDP, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with ( @@ -122,21 +122,21 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USE_PSK: False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pin" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -157,7 +157,7 @@ async def test_ssdp_discovery_fake(hass: HomeAssistant) -> None: data=FAKE_BRAVIA_SSDP, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_bravia_device" @@ -181,7 +181,7 @@ async def test_ssdp_discovery_exist(hass: HomeAssistant) -> None: data=BRAVIA_SSDP, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -261,7 +261,7 @@ async def test_no_ip_control(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_USE_PSK: False} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_ip_control" @@ -298,7 +298,7 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -319,21 +319,21 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USE_PSK: False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pin" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -360,21 +360,21 @@ async def test_create_entry_psk(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USE_PSK: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "psk" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "mypsk"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -427,7 +427,7 @@ async def test_reauth_successful(hass: HomeAssistant, use_psk, new_pin) -> None: data=config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( @@ -437,6 +437,6 @@ async def test_reauth_successful(hass: HomeAssistant, use_psk, new_pin) -> None: result["flow_id"], user_input={CONF_PIN: new_pin} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PIN] == new_pin diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index 29abad94fad..351ba533101 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_init( @@ -44,7 +44,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DATA_STEP["email"] assert result["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -73,7 +73,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == text_error # Recover @@ -86,7 +86,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == MOCK_DATA_STEP["email"] assert result["data"] == MOCK_DATA_STEP @@ -110,5 +110,5 @@ async def test_flow_user_init_data_already_configured( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index 6bf9fd1cb54..db402bdd6d1 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -8,10 +8,12 @@ from homeassistant.components.bring import ( BringAuthException, BringParseException, BringRequestException, + async_setup_entry, ) from homeassistant.components.bring.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from tests.common import MockConfigEntry @@ -37,10 +39,10 @@ async def test_load_unload( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert bring_config_entry.state == ConfigEntryState.LOADED + assert bring_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(bring_config_entry.entry_id) - assert bring_config_entry.state == ConfigEntryState.NOT_LOADED + assert bring_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -62,3 +64,26 @@ async def test_init_failure( mock_bring_client.login.side_effect = exception await setup_integration(hass, bring_config_entry) assert bring_config_entry.state == status + + +@pytest.mark.parametrize( + ("exception", "expected"), + [ + (BringRequestException, ConfigEntryNotReady), + (BringAuthException, ConfigEntryError), + (BringParseException, ConfigEntryNotReady), + ], +) +async def test_init_exceptions( + hass: HomeAssistant, + mock_bring_client: AsyncMock, + exception: Exception, + expected: Exception, + bring_config_entry: MockConfigEntry | None, +) -> None: + """Test an initialization error on integration load.""" + bring_config_entry.add_to_hass(hass) + mock_bring_client.login.side_effect = exception + + with pytest.raises(expected): + await async_setup_entry(hass, bring_config_entry) diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 143742d3a9a..2def8c0b3b9 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.broadlink.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import get_device @@ -42,7 +43,7 @@ async def test_flow_user_works(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +53,7 @@ async def test_flow_user_works(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "finish" assert result["errors"] == {} @@ -61,7 +62,7 @@ async def test_flow_user_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -93,7 +94,7 @@ async def test_flow_user_already_in_progress(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -120,7 +121,7 @@ async def test_flow_user_mac_already_configured(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert dict(mock_entry.data) == device.get_entry_data() @@ -139,7 +140,7 @@ async def test_flow_user_invalid_ip_address(hass: HomeAssistant) -> None: {"host": "0.0.0.1"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -156,7 +157,7 @@ async def test_flow_user_invalid_hostname(hass: HomeAssistant) -> None: {"host": "pancakemaster.local"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -175,7 +176,7 @@ async def test_flow_user_device_not_found(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -195,7 +196,7 @@ async def test_flow_user_device_not_supported(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -211,7 +212,7 @@ async def test_flow_user_network_unreachable(hass: HomeAssistant) -> None: {"host": "192.168.1.32"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -228,7 +229,7 @@ async def test_flow_user_os_error(hass: HomeAssistant) -> None: {"host": "192.168.1.32"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -249,7 +250,7 @@ async def test_flow_auth_authentication_error(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reset" assert result["errors"] == {"base": "invalid_auth"} @@ -270,7 +271,7 @@ async def test_flow_auth_network_timeout(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -291,7 +292,7 @@ async def test_flow_auth_firmware_error(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -312,7 +313,7 @@ async def test_flow_auth_network_unreachable(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -333,7 +334,7 @@ async def test_flow_auth_os_error(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -365,7 +366,7 @@ async def test_flow_reset_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -386,7 +387,7 @@ async def test_flow_unlock_works(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {} @@ -400,7 +401,7 @@ async def test_flow_unlock_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -430,7 +431,7 @@ async def test_flow_unlock_network_timeout(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "cannot_connect"} @@ -457,7 +458,7 @@ async def test_flow_unlock_firmware_error(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "unknown"} @@ -484,7 +485,7 @@ async def test_flow_unlock_network_unreachable(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "cannot_connect"} @@ -511,7 +512,7 @@ async def test_flow_unlock_os_error(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "unknown"} @@ -542,7 +543,7 @@ async def test_flow_do_not_unlock(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -561,7 +562,7 @@ async def test_flow_import_works(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "finish" assert result["errors"] == {} @@ -570,7 +571,7 @@ async def test_flow_import_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"]["host"] == device.host assert result["data"]["mac"] == device.mac @@ -595,7 +596,7 @@ async def test_flow_import_already_in_progress(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -613,7 +614,7 @@ async def test_flow_import_host_already_configured(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -636,7 +637,7 @@ async def test_flow_import_mac_already_configured(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data["host"] == device.host @@ -654,7 +655,7 @@ async def test_flow_import_device_not_found(hass: HomeAssistant) -> None: data={"host": "192.168.1.32"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -670,7 +671,7 @@ async def test_flow_import_device_not_supported(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -683,7 +684,7 @@ async def test_flow_import_invalid_ip_address(hass: HomeAssistant) -> None: data={"host": "0.0.0.1"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -696,7 +697,7 @@ async def test_flow_import_invalid_hostname(hass: HomeAssistant) -> None: data={"host": "hotdog.local"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -709,7 +710,7 @@ async def test_flow_import_network_unreachable(hass: HomeAssistant) -> None: data={"host": "192.168.1.64"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -722,7 +723,7 @@ async def test_flow_import_os_error(hass: HomeAssistant) -> None: data={"host": "192.168.1.64"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -740,7 +741,7 @@ async def test_flow_reauth_works(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reset" mock_api = device.get_mock_api() @@ -751,7 +752,7 @@ async def test_flow_reauth_works(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert dict(mock_entry.data) == device.get_entry_data() @@ -785,7 +786,7 @@ async def test_flow_reauth_invalid_host(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -819,7 +820,7 @@ async def test_flow_reauth_valid_host(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data["host"] == device.host @@ -846,7 +847,7 @@ async def test_dhcp_can_finish(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "finish" result2 = await hass.config_entries.flow.async_configure( @@ -855,7 +856,7 @@ async def test_dhcp_can_finish(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Living Room" assert result2["data"] == { "host": "1.2.3.4", @@ -880,7 +881,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -899,7 +900,7 @@ async def test_dhcp_unreachable(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -918,7 +919,7 @@ async def test_dhcp_connect_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -939,7 +940,7 @@ async def test_dhcp_device_not_supported(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -964,7 +965,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -989,6 +990,6 @@ async def test_dhcp_updates_host(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data["host"] == "4.5.6.7" diff --git a/tests/components/brother/snapshots/test_sensor.ambr b/tests/components/brother/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a27c5addd61 --- /dev/null +++ b/tests/components/brother/snapshots/test_sensor.ambr @@ -0,0 +1,1394 @@ +# serializer version: 1 +# name: test_sensors[sensor.hl_l2340dw_b_w_pages-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.hl_l2340dw_b_w_pages', + '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': 'B/W pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bw_pages', + 'unique_id': '0123456789_bw_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_b_w_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW B/W pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_b_w_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '709', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_belt_unit_remaining_lifetime-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.hl_l2340dw_belt_unit_remaining_lifetime', + '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': 'Belt unit remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'belt_unit_remaining_life', + 'unique_id': '0123456789_belt_unit_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_belt_unit_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Belt unit remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_belt_unit_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_page_counter-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.hl_l2340dw_black_drum_page_counter', + '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': 'Black drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_drum_page_counter', + 'unique_id': '0123456789_black_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_lifetime-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.hl_l2340dw_black_drum_remaining_lifetime', + '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': 'Black drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_drum_remaining_life', + 'unique_id': '0123456789_black_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_pages-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.hl_l2340dw_black_drum_remaining_pages', + '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': 'Black drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_drum_remaining_pages', + 'unique_id': '0123456789_black_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_toner_remaining-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.hl_l2340dw_black_toner_remaining', + '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': 'Black toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_toner_remaining', + 'unique_id': '0123456789_black_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_color_pages-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.hl_l2340dw_color_pages', + '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': 'Color pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'color_pages', + 'unique_id': '0123456789_color_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_color_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Color pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_color_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '902', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_page_counter-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.hl_l2340dw_cyan_drum_page_counter', + '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': 'Cyan drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_drum_page_counter', + 'unique_id': '0123456789_cyan_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_lifetime-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.hl_l2340dw_cyan_drum_remaining_lifetime', + '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': 'Cyan drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_drum_remaining_life', + 'unique_id': '0123456789_cyan_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_pages-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.hl_l2340dw_cyan_drum_remaining_pages', + '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': 'Cyan drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_drum_remaining_pages', + 'unique_id': '0123456789_cyan_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_toner_remaining-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.hl_l2340dw_cyan_toner_remaining', + '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': 'Cyan toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_toner_remaining', + 'unique_id': '0123456789_cyan_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_page_counter-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.hl_l2340dw_drum_page_counter', + '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': 'Drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drum_page_counter', + 'unique_id': '0123456789_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '986', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_lifetime-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.hl_l2340dw_drum_remaining_lifetime', + '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': 'Drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drum_remaining_life', + 'unique_id': '0123456789_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_pages-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.hl_l2340dw_drum_remaining_pages', + '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': 'Drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drum_remaining_pages', + 'unique_id': '0123456789_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11014', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_duplex_unit_page_counter-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.hl_l2340dw_duplex_unit_page_counter', + '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': 'Duplex unit page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'duplex_unit_page_counter', + 'unique_id': '0123456789_duplex_unit_pages_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_duplex_unit_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Duplex unit page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_duplex_unit_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '538', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_fuser_remaining_lifetime-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.hl_l2340dw_fuser_remaining_lifetime', + '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': 'Fuser remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fuser_remaining_life', + 'unique_id': '0123456789_fuser_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_fuser_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Fuser remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_fuser_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_last_restart-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.hl_l2340dw_last_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last restart', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_restart', + 'unique_id': '0123456789_uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_last_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'HL-L2340DW Last restart', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_last_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-03T15:04:24+00:00', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_page_counter-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.hl_l2340dw_magenta_drum_page_counter', + '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': 'Magenta drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_drum_page_counter', + 'unique_id': '0123456789_magenta_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_lifetime-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.hl_l2340dw_magenta_drum_remaining_lifetime', + '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': 'Magenta drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_drum_remaining_life', + 'unique_id': '0123456789_magenta_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_pages-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.hl_l2340dw_magenta_drum_remaining_pages', + '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': 'Magenta drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_drum_remaining_pages', + 'unique_id': '0123456789_magenta_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_toner_remaining-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.hl_l2340dw_magenta_toner_remaining', + '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': 'Magenta toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_toner_remaining', + 'unique_id': '0123456789_magenta_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_page_counter-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.hl_l2340dw_page_counter', + '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': 'Page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'page_counter', + 'unique_id': '0123456789_page_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '986', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_pf_kit_1_remaining_lifetime-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.hl_l2340dw_pf_kit_1_remaining_lifetime', + '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': 'PF Kit 1 remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pf_kit_1_remaining_life', + 'unique_id': '0123456789_pf_kit_1_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_pf_kit_1_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW PF Kit 1 remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_pf_kit_1_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '98', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_status-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.hl_l2340dw_status', + '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', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': '0123456789_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Status', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'waiting', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_page_counter-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.hl_l2340dw_yellow_drum_page_counter', + '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': 'Yellow drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_drum_page_counter', + 'unique_id': '0123456789_yellow_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_lifetime-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.hl_l2340dw_yellow_drum_remaining_lifetime', + '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': 'Yellow drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_drum_remaining_life', + 'unique_id': '0123456789_yellow_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_pages-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.hl_l2340dw_yellow_drum_remaining_pages', + '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': 'Yellow drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_drum_remaining_pages', + 'unique_id': '0123456789_yellow_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_toner_remaining-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.hl_l2340dw_yellow_toner_remaining', + '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': 'Yellow toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_toner_remaining', + 'unique_id': '0123456789_yellow_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 2eff4ed2770..a476ec8f579 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -7,12 +7,12 @@ from unittest.mock import patch from brother import SnmpError, UnsupportedModelError import pytest -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_fixture @@ -27,7 +27,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -46,7 +46,7 @@ async def test_create_entry_with_hostname(hass: HomeAssistant) -> None: data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "example.local" assert result["data"][CONF_TYPE] == "laser" @@ -65,7 +65,7 @@ async def test_create_entry_with_ipv4_address(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" @@ -86,7 +86,7 @@ async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: data={CONF_HOST: "2001:db8::1428:57ab", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "2001:db8::1428:57ab" assert result["data"][CONF_TYPE] == "laser" @@ -140,7 +140,7 @@ async def test_unsupported_model_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_model" @@ -160,7 +160,7 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -209,7 +209,7 @@ async def test_zeroconf_unsupported_model(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_model" assert len(mock_get_data.mock_calls) == 0 @@ -244,7 +244,7 @@ async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -274,7 +274,7 @@ async def test_zeroconf_no_probe_existing_device(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_get_data.mock_calls) == 0 @@ -305,13 +305,13 @@ async def test_zeroconf_confirm_create_entry(hass: HomeAssistant) -> None: assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"]["model"] == "HL-L2340DW" assert result["description_placeholders"]["serial_number"] == "0123456789" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TYPE: "laser"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index ff29f8cb368..069a5ddc152 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -1,389 +1,40 @@ """Test sensor of Brother integration.""" -from datetime import datetime, timedelta +from datetime import timedelta import json -from unittest.mock import Mock, patch +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion from homeassistant.components.brother.const import DOMAIN -from homeassistant.components.brother.sensor import UNIT_PAGES -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ENTITY_ID, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - PERCENTAGE, - STATE_UNAVAILABLE, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util.dt import UTC, utcnow +from homeassistant.util.dt import utcnow from . import init_integration -from tests.common import async_fire_time_changed, load_fixture - -ATTR_REMAINING_PAGES = "remaining_pages" -ATTR_COUNTER = "counter" +from tests.common import async_fire_time_changed, load_fixture, snapshot_platform -async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: - """Test states of the sensors.""" - entry = await init_integration(hass, skip_setup=True) - - # Pre-create registry entries for disabled by default sensors - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "0123456789_uptime", - suggested_object_id="hl_l2340dw_last_restart", - 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")), - ), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.hl_l2340dw_status") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.state == "waiting" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.hl_l2340dw_status") - assert entry - assert entry.unique_id == "0123456789_status" - - state = hass.states.get("sensor.hl_l2340dw_black_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_black_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_cyan_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_cyan_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_magenta_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "8" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_magenta_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_yellow_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "2" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_yellow_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_drum_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "11014" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "986" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_drum_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_black_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_black_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_black_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_black_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_cyan_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_cyan_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_cyan_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get( - "sensor.hl_l2340dw_magenta_drum_remaining_lifetime" - ) - assert entry - assert entry.unique_id == "0123456789_magenta_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_magenta_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_magenta_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_magenta_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get( - "sensor.hl_l2340dw_yellow_drum_remaining_lifetime" - ) - assert entry - assert entry.unique_id == "0123456789_yellow_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_yellow_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_yellow_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_yellow_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "97" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_fuser_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_fuser_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_belt_unit_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "97" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_belt_unit_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_pf_kit_1_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "98" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_pf_kit_1_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "986" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_page_counter") - assert entry - assert entry.unique_id == "0123456789_page_counter" - - state = hass.states.get("sensor.hl_l2340dw_duplex_unit_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "538" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_duplex_unit_page_counter") - assert entry - assert entry.unique_id == "0123456789_duplex_unit_pages_counter" - - state = hass.states.get("sensor.hl_l2340dw_b_w_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "709" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_b_w_pages") - assert entry - assert entry.unique_id == "0123456789_bw_counter" - - state = hass.states.get("sensor.hl_l2340dw_color_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "902" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_color_pages") - assert entry - assert entry.unique_id == "0123456789_color_counter" - - state = hass.states.get("sensor.hl_l2340dw_last_restart") - 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_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - assert state.state == "2019-09-24T12:14:56+00:00" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.hl_l2340dw_last_restart") - assert entry - assert entry.unique_id == "0123456789_uptime" - - -async def test_disabled_by_default_sensors( - hass: HomeAssistant, entity_registry: er.EntityRegistry +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + entity_registry_enabled_by_default: None, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the disabled by default Brother sensors.""" - await init_integration(hass) + """Test states of the sensors.""" + hass.config.set_time_zone("UTC") + freezer.move_to("2024-04-20 12:00:00+00:00") - state = hass.states.get("sensor.hl_l2340dw_last_restart") - assert state is None + with patch("homeassistant.components.brother.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - entry = entity_registry.async_get("sensor.hl_l2340dw_last_restart") - assert entry - assert entry.unique_id == "0123456789_uptime" - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: diff --git a/tests/components/brottsplatskartan/test_config_flow.py b/tests/components/brottsplatskartan/test_config_flow.py index 3571277c6f3..41ac297961f 100644 --- a/tests/components/brottsplatskartan/test_config_flow.py +++ b/tests/components/brottsplatskartan/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Brottsplatskartan HOME" assert result2["data"] == { "area": None, @@ -44,7 +44,7 @@ async def test_form_location(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -58,7 +58,7 @@ async def test_form_location(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Brottsplatskartan 59.32, 18.06" assert result2["data"] == { "area": None, @@ -74,7 +74,7 @@ async def test_form_area(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_form_area(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Brottsplatskartan Stockholms län" assert result2["data"] == { "latitude": None, diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index dfa1e9f992a..2796882a3c1 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -6,10 +6,11 @@ from aiohttp import ClientResponseError from aiohttp.client_exceptions import ServerDisconnectedError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.brunt.const import DOMAIN 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 @@ -23,7 +24,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -58,7 +59,7 @@ async def test_form_duplicate_login(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -81,17 +82,17 @@ async def test_form_error(hass: HomeAssistant, side_effect, error_message) -> No DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error_message} @pytest.mark.parametrize( ("side_effect", "result_type", "password", "step_id", "reason"), [ - (None, data_entry_flow.FlowResultType.ABORT, "test", None, "reauth_successful"), + (None, FlowResultType.ABORT, "test", None, "reauth_successful"), ( Exception, - data_entry_flow.FlowResultType.FORM, + FlowResultType.FORM, CONFIG[CONF_PASSWORD], "reauth_confirm", None, @@ -118,7 +119,7 @@ async def test_reauth( }, data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index db2b0f8f85c..91e4338d688 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -26,7 +26,7 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -40,7 +40,7 @@ async def test_full_user_flow_implementation( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == format_mac("00:80:41:19:69:90") assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -64,7 +64,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_connection_error( @@ -86,7 +86,7 @@ async def test_connection_error( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -110,5 +110,5 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index 1a785858752..acf490d341e 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -27,13 +27,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ATC 18B2" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:8D:18:B2" @@ -56,7 +56,7 @@ async def test_async_step_bluetooth_during_onboarding(hass: HomeAssistant) -> No data=TEMP_HUMI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ATC 18B2" assert result["data"] == {} assert result["result"].unique_id == "A4:C1:38:8D:18:B2" @@ -73,7 +73,7 @@ async def test_async_step_bluetooth_valid_device_with_encryption( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): @@ -81,7 +81,7 @@ async def test_async_step_bluetooth_valid_device_with_encryption( result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -96,14 +96,14 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -113,7 +113,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -128,7 +128,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key" result2 = await hass.config_entries.flow.async_configure( @@ -136,7 +136,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length( user_input={"bindkey": "aa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -146,7 +146,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length( result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -159,7 +159,7 @@ async def test_async_step_bluetooth_not_supported(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_BTHOME_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -169,7 +169,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -186,7 +186,7 @@ async def test_async_step_user_no_devices_found_2(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -200,14 +200,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "b-parasite 80A5" assert result2["data"] == {} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -225,14 +225,14 @@ async def test_async_step_user_with_found_devices_encryption( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): @@ -241,7 +241,7 @@ async def test_async_step_user_with_found_devices_encryption( user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -260,7 +260,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Pick a device @@ -268,7 +268,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key" # Try an incorrect key @@ -276,7 +276,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -287,7 +287,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -306,7 +306,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Select a single device @@ -314,7 +314,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key" # Try an incorrect key @@ -323,7 +323,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( user_input={"bindkey": "aa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -334,7 +334,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -350,7 +350,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -364,7 +364,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "A4:C1:38:8D:18:B2"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -386,7 +386,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -403,7 +403,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -414,7 +414,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -422,7 +422,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -435,7 +435,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -446,14 +446,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "b-parasite 80A5" assert result2["data"] == {} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -498,7 +498,7 @@ async def test_async_step_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -538,7 +538,7 @@ async def test_async_step_reauth_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -546,7 +546,7 @@ async def test_async_step_reauth_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -574,5 +574,5 @@ async def test_async_step_reauth_abort_early(hass: HomeAssistant) -> None: data=entry.data | {"device": device}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index 6a7db5e9066..316cb90a348 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -2,10 +2,11 @@ import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +22,7 @@ async def test_config_flow_setup_(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -30,7 +31,7 @@ async def test_config_flow_setup_(hass: HomeAssistant) -> None: {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" assert result["data"] == { CONF_LATITUDE: TEST_LATITUDE, @@ -54,7 +55,7 @@ async def test_config_flow_already_configured_weather(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -63,7 +64,7 @@ async def test_config_flow_already_configured_weather(hass: HomeAssistant) -> No {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -84,7 +85,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -92,7 +93,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"country_code": "BE", "delta": 450, "timeframe": 30}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/caldav/test_config_flow.py b/tests/components/caldav/test_config_flow.py index 6af7d5c670c..c6d5552c874 100644 --- a/tests/components/caldav/test_config_flow.py +++ b/tests/components/caldav/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -49,7 +49,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_USERNAME assert result2.get("data") == { CONF_URL: TEST_URL, @@ -93,7 +93,7 @@ async def test_caldav_client_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": expected_error} @@ -113,7 +113,7 @@ async def test_reauth_success( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -124,7 +124,7 @@ async def test_reauth_success( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" # Verify updated configuration entry @@ -154,7 +154,7 @@ async def test_reauth_failure( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" dav_client.return_value.principal.side_effect = DAVError @@ -167,7 +167,7 @@ async def test_reauth_failure( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "cannot_connect"} # Complete the form and it succeeds this time @@ -180,7 +180,7 @@ async def test_reauth_failure( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" # Verify updated configuration entry @@ -223,7 +223,7 @@ async def test_multiple_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_multiple_config_entries( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == user_input[CONF_USERNAME] assert result2.get("data") == { **user_input, @@ -271,7 +271,7 @@ async def test_duplicate_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -280,5 +280,5 @@ async def test_duplicate_config_entries( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/caldav/test_init.py b/tests/components/caldav/test_init.py index 192c18ef81a..f49b1275dca 100644 --- a/tests/components/caldav/test_init.py +++ b/tests/components/caldav/test_init.py @@ -23,15 +23,15 @@ async def test_load_unload( config_entry: MockConfigEntry, ) -> None: """Test loading and unloading of the config entry.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with patch("homeassistant.components.caldav.config_flow.caldav.DAVClient"): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -57,7 +57,7 @@ async def test_client_failure( ) -> None: """Test CalDAV client failures in setup.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.caldav.config_flow.caldav.DAVClient" diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 67fc5f7f443..bea4725856e 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -624,7 +624,7 @@ async def test_remove_item( assert state.state == "1" def lookup(uid: str) -> Mock: - assert uid == "2" or uid == "3" + assert uid in ("2", "3") if uid == "2": return item1 return item2 diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 050329cd855..54cfd353618 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -20,8 +20,7 @@ import zoneinfo from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components import calendar -import homeassistant.components.automation as automation +from homeassistant.components import automation, calendar from homeassistant.components.calendar.trigger import EVENT_END, EVENT_START from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index ccec2b6f50c..dffc7e5aa53 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -24,10 +24,15 @@ 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 homeassistant.util import dt as dt_util from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_turbo_jpeg -from tests.common import help_test_all, import_and_test_deprecated_constant_enum +from tests.common import ( + async_fire_time_changed, + help_test_all, + import_and_test_deprecated_constant_enum, +) from tests.typing import ClientSessionGenerator, WebSocketGenerator STREAM_SOURCE = "rtsp://127.0.0.1/stream" @@ -1073,3 +1078,23 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> caplog.clear() assert entity.supported_features_compat is camera.CameraEntityFeature(1) assert "is using deprecated supported features values" not in caplog.text + + +async def test_entity_picture_url_changes_on_token_update( + hass: HomeAssistant, mock_camera +) -> None: + """Test the token is rotated and entity entity picture cache is cleared.""" + await async_setup_component(hass, "camera", {}) + await hass.async_block_till_done() + + camera_state = hass.states.get("camera.demo_camera") + original_picture = camera_state.attributes["entity_picture"] + assert "token=" in original_picture + + async_fire_time_changed(hass, dt_util.utcnow() + camera.TOKEN_CHANGE_INTERVAL) + await hass.async_block_till_done(wait_background_tasks=True) + + camera_state = hass.states.get("camera.demo_camera") + new_entity_picture = camera_state.attributes["entity_picture"] + assert new_entity_picture != original_picture + assert "token=" in new_entity_picture diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index 3c32c683a39..552aa9089ce 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_form(hass: HomeAssistant, canary_config_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -37,7 +37,7 @@ async def test_user_form(hass: HomeAssistant, canary_config_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT} @@ -60,7 +60,7 @@ async def test_user_form_cannot_connect( USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} canary_config_flow.side_effect = ConnectTimeout() @@ -70,7 +70,7 @@ async def test_user_form_cannot_connect( USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_user_form_unexpected_exception( USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -104,7 +104,7 @@ async def test_user_form_single_instance_allowed( context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -117,7 +117,7 @@ async def test_options_flow(hass: HomeAssistant, canary) -> None: assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup(), _patch_async_setup_entry(): @@ -127,6 +127,6 @@ async def test_options_flow(hass: HomeAssistant, canary) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" assert result["data"][CONF_TIMEOUT] == 7 diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index d9ebb24696e..66a55ba7efd 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -18,8 +18,7 @@ def get_multizone_status_mock(): @pytest.fixture def get_cast_type_mock(): """Mock pychromecast dial.""" - mock = MagicMock(spec_set=pychromecast.dial.get_cast_type) - return mock + return MagicMock(spec_set=pychromecast.dial.get_cast_type) @pytest.fixture diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index a7b9311e88b..2c0c36d6632 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import ANY, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import cast from homeassistant.components.cast.home_assistant_cast import CAST_USER_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,10 +30,10 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -54,7 +55,7 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": source} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -63,13 +64,13 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": [], @@ -83,7 +84,7 @@ async def test_user_setup_options(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "} @@ -91,7 +92,7 @@ async def test_user_setup_options(hass: HomeAssistant) -> None: users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": ["192.168.0.1", "192.168.0.2"], @@ -105,13 +106,13 @@ async def test_zeroconf_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": [], @@ -131,7 +132,7 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": [], @@ -194,7 +195,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} @@ -205,7 +206,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema for other_param in basic_parameters: @@ -222,7 +223,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "advanced_options" for other_param in basic_parameters: if other_param == parameter: @@ -247,7 +248,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] is None for other_param in advanced_parameters: if other_param == parameter: @@ -261,7 +262,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result["flow_id"], user_input={"known_hosts": ""}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] is None expected_data = {**orig_data, "known_hosts": []} if parameter in advanced_parameters: @@ -277,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done(wait_background_tasks=True) config_entry = hass.config_entries.async_entries("cast")[0] diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 8381f27398a..5481459b715 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -271,9 +271,11 @@ async def test_start_discovery_called_once( ) -> None: """Test pychromecast.start_discovery called exactly once.""" await async_setup_cast(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.start_discovery.call_count == 1 await async_setup_cast(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.start_discovery.call_count == 1 @@ -453,11 +455,13 @@ async def test_stop_discovery_called_on_stop( """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config await async_setup_cast(hass, {}) + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.start_discovery.call_count == 1 # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - 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 castbrowser_mock.return_value.stop_discovery.call_count == 1 diff --git a/tests/components/ccm15/test_config_flow.py b/tests/components/ccm15/test_config_flow.py index 87c93179f4e..01da3282885 100644 --- a/tests/components/ccm15/test_config_flow.py +++ b/tests/components/ccm15/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -47,7 +47,7 @@ async def test_form_invalid_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -62,7 +62,7 @@ async def test_form_invalid_host( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -76,7 +76,7 @@ async def test_form_invalid_host( }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -95,7 +95,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_unexpected_error(hass: HomeAssistant) -> None: @@ -128,7 +128,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with patch( @@ -141,7 +141,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_duplicate_host(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @@ -168,5 +168,5 @@ async def test_duplicate_host(hass: HomeAssistant, mock_setup_entry: AsyncMock) }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index aa5f32c0ca2..3fd696f5953 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import HOST, PORT from .helpers import future_timestamp @@ -24,7 +25,7 @@ async def test_user(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -33,7 +34,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -45,7 +46,7 @@ async def test_user_with_bad_cert(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -56,7 +57,7 @@ async def test_user_with_bad_cert(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -81,7 +82,7 @@ async def test_import_host_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT @@ -106,7 +107,7 @@ async def test_import_host_and_port(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -131,7 +132,7 @@ async def test_import_non_default_port(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{HOST}:888" assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 @@ -156,7 +157,7 @@ async def test_import_with_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -175,7 +176,7 @@ async def test_bad_import(hass: HomeAssistant) -> None: data={CONF_HOST: HOST}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -192,7 +193,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -200,7 +201,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -217,7 +218,7 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "resolve_failed"} with patch( @@ -227,7 +228,7 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} with patch( @@ -237,5 +238,5 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_refused"} diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 312b87affd3..e2c333cc6f3 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -35,12 +35,6 @@ async def test_setup_with_config(hass: HomeAssistant) -> None: {"platform": DOMAIN, CONF_HOST: HOST, CONF_PORT: 888}, ], } - assert await async_setup_component(hass, SENSOR_DOMAIN, config) is True - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - next_update = dt_util.utcnow() + timedelta(seconds=20) - async_fire_time_changed(hass, next_update) with ( patch( @@ -51,7 +45,13 @@ async def test_setup_with_config(hass: HomeAssistant) -> None: return_value=future_timestamp(1), ), ): + assert await async_setup_component(hass, SENSOR_DOMAIN, config) is True await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done(wait_background_tasks=True) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3ee5a9b8edd..850f8b6c843 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.climate import DOMAIN, HVACMode, const, device_action from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import EntityCategory diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index d9345a0516c..e44802f7d4d 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.climate import DOMAIN, HVACMode, const, device_condition from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import EntityCategory diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 7dbe106bd4f..af14c42c086 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.climate import ( DOMAIN, HVACAction, diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 99a21734588..024118eaabf 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -7,9 +7,10 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.cloud import account_link from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow @@ -203,7 +204,7 @@ async def test_implementation( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == "http://example.com/auth" flow_finished.set_result( diff --git a/tests/components/cloud/test_assist_pipeline.py b/tests/components/cloud/test_assist_pipeline.py index 5c2fc074898..de30212c040 100644 --- a/tests/components/cloud/test_assist_pipeline.py +++ b/tests/components/cloud/test_assist_pipeline.py @@ -7,10 +7,12 @@ from homeassistant.components.cloud.assist_pipeline import ( ) from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component async def test_migrate_pipeline_invalid_platform(hass: HomeAssistant) -> None: """Test migrate pipeline with invalid platform.""" + await async_setup_component(hass, "assist_pipeline", {}) with pytest.raises(ValueError): await async_migrate_cloud_pipeline_engine( hass, Platform.BINARY_SENSOR, "test-engine-id" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 5e15aa32b6f..bcddc32f107 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -24,6 +24,7 @@ from homeassistant.components.homeassistant.exposed_entities import ( ExposedEntities, async_expose_entity, ) +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.const import CONTENT_TYPE_JSON, __version__ as HA_VERSION from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -387,6 +388,7 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: "connected": False, "enabled": False, "instance_domain": None, + "strict_connection": StrictConnectionMode.DISABLED, }, "version": HA_VERSION, } diff --git a/tests/components/cloud/test_config_flow.py b/tests/components/cloud/test_config_flow.py index 6b506d6b883..9f6e1762482 100644 --- a/tests/components/cloud/test_config_flow.py +++ b/tests/components/cloud/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant.components.cloud.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +24,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Cloud" assert result["data"] == {} await hass.async_block_till_done() @@ -40,5 +41,5 @@ async def test_multiple_entries(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 0dad7cfa882..1e4dc3173e2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -19,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.http.const import StrictConnectionMode from homeassistant.components.websocket_api import ERR_INVALID_FORMAT from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -231,6 +232,7 @@ async def test_login_view_create_pipeline( } assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "assist_pipeline", {}) assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) await hass.async_block_till_done() @@ -270,6 +272,7 @@ async def test_login_view_create_pipeline_fail( } assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "assist_pipeline", {}) assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) await hass.async_block_till_done() @@ -780,6 +783,7 @@ async def test_websocket_status( "google_report_state": True, "remote_allow_remote_enable": True, "remote_enabled": False, + "strict_connection": "disabled", "tts_default_voice": ["en-US", "JennyNeural"], }, "alexa_entities": { @@ -899,6 +903,7 @@ async def test_websocket_update_preferences( assert cloud.client.prefs.alexa_enabled assert cloud.client.prefs.google_secure_devices_pin is None assert cloud.client.prefs.remote_allow_remote_enable is True + assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED client = await hass_ws_client(hass) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 9cc1324ebc1..d917dc12a7c 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import MagicMock, patch +from urllib.parse import quote_plus from hass_nabucasa import Cloud import pytest @@ -13,11 +14,16 @@ from homeassistant.components.cloud import ( CloudNotConnected, async_get_or_create_cloudhook, ) -from homeassistant.components.cloud.const import DOMAIN, PREF_CLOUDHOOKS +from homeassistant.components.cloud.const import ( + DOMAIN, + PREF_CLOUDHOOKS, + PREF_STRICT_CONNECTION, +) from homeassistant.components.cloud.prefs import STORAGE_KEY +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import ServiceValidationError, Unauthorized from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockUser @@ -295,3 +301,79 @@ async def test_cloud_logout( await hass.async_block_till_done() assert cloud.is_logged_in is False + + +@pytest.mark.skip(reason="Remove strict connection config option") +async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( + hass: HomeAssistant, +) -> None: + """Test service create_temporary_strict_connection_url with strict_connection not enabled.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + mock_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + with pytest.raises( + ServiceValidationError, + match="Strict connection is not enabled for cloud requests", + ): + await hass.services.async_call( + cloud.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + +@pytest.mark.skip(reason="Remove strict connection config option") +@pytest.mark.parametrize( + ("mode"), + [ + StrictConnectionMode.DROP_CONNECTION, + StrictConnectionMode.GUARD_PAGE, + ], +) +async def test_service_create_temporary_strict_connection( + hass: HomeAssistant, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + mode: StrictConnectionMode, +) -> None: + """Test service create_temporary_strict_connection_url.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + mock_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + await set_cloud_prefs( + { + PREF_STRICT_CONNECTION: mode, + } + ) + + # No cloud url set + with pytest.raises(ServiceValidationError, match="No cloud URL available"): + await hass.services.async_call( + cloud.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + # Patch cloud url + url = "https://example.com" + with patch( + "homeassistant.helpers.network._get_cloud_url", + return_value=url, + ): + response = await hass.services.async_call( + cloud.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + assert isinstance(response, dict) + direct_url_prefix = f"{url}/auth/strict_connection/temp_token?authSig=" + assert response.pop("direct_url").startswith(direct_url_prefix) + assert response.pop("url").startswith( + f"https://login.home-assistant.io?u={quote_plus(direct_url_prefix)}" + ) + assert response == {} # No more keys in response diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 9b0fa4c01d7..a8ce88f5700 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -6,8 +6,13 @@ 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.const import ( + DOMAIN, + PREF_STRICT_CONNECTION, + PREF_TTS_DEFAULT_VOICE, +) from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -174,3 +179,40 @@ async def test_tts_default_voice_legacy_gender( await hass.async_block_till_done() assert cloud.client.prefs.tts_default_voice == (expected_language, voice) + + +@pytest.mark.skip(reason="Remove strict connection config option") +@pytest.mark.parametrize("mode", list(StrictConnectionMode)) +async def test_strict_connection_convertion( + hass: HomeAssistant, + cloud: MagicMock, + hass_storage: dict[str, Any], + mode: StrictConnectionMode, +) -> None: + """Test strict connection string value will be converted to the enum.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "data": {PREF_STRICT_CONNECTION: mode.value}, + } + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert cloud.client.prefs.strict_connection is mode + + +@pytest.mark.parametrize("storage_data", [{}, {PREF_STRICT_CONNECTION: None}]) +async def test_strict_connection_default( + hass: HomeAssistant, + cloud: MagicMock, + hass_storage: dict[str, Any], + storage_data: dict[str, Any], +) -> None: + """Test strict connection default values.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "data": storage_data, + } + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED diff --git a/tests/components/cloud/test_strict_connection.py b/tests/components/cloud/test_strict_connection.py new file mode 100644 index 00000000000..c3329740207 --- /dev/null +++ b/tests/components/cloud/test_strict_connection.py @@ -0,0 +1,295 @@ +"""Test strict connection mode for cloud.""" + +from collections.abc import Awaitable, Callable, Coroutine, Generator +from contextlib import contextmanager +from datetime import timedelta +from http import HTTPStatus +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +from aiohttp import ServerDisconnectedError, web +from aiohttp.test_utils import TestClient +from aiohttp_session import get_session +import pytest +from yarl import URL + +from homeassistant.auth.models import RefreshToken +from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT +from homeassistant.components.cloud.const import PREF_STRICT_CONNECTION +from homeassistant.components.http import KEY_HASS +from homeassistant.components.http.auth import ( + STRICT_CONNECTION_GUARD_PAGE, + async_setup_auth, + async_sign_path, +) +from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode +from homeassistant.components.http.session import COOKIE_NAME, PREFIXED_COOKIE_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.network import is_cloud_connection +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.typing import ClientSessionGenerator + + +@pytest.fixture +async def refresh_token(hass: HomeAssistant, hass_access_token: str) -> RefreshToken: + """Return a refresh token.""" + refresh_token = hass.auth.async_validate_access_token(hass_access_token) + assert refresh_token + session = hass.auth.session + assert session._strict_connection_sessions == {} + assert session._temp_sessions == {} + return refresh_token + + +@contextmanager +def simulate_cloud_request() -> Generator[None, None, None]: + """Simulate a cloud request.""" + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + yield + + +@pytest.fixture +def app_strict_connection( + hass: HomeAssistant, refresh_token: RefreshToken +) -> web.Application: + """Fixture to set up a web.Application.""" + + async def handler(request): + """Return if request was authenticated.""" + return web.json_response(data={"authenticated": request[KEY_AUTHENTICATED]}) + + app = web.Application() + app[KEY_HASS] = hass + app.router.add_get("/", handler) + + async def set_cookie(request: web.Request) -> web.Response: + hass = request.app[KEY_HASS] + # Clear all sessions + hass.auth.session._temp_sessions.clear() + hass.auth.session._strict_connection_sessions.clear() + + if request.query["token"] == "refresh": + await hass.auth.session.async_create_session(request, refresh_token) + else: + await hass.auth.session.async_create_temp_unauthorized_session(request) + session = await get_session(request) + return web.Response(text=session[SESSION_ID]) + + app.router.add_get("/test/cookie", set_cookie) + return app + + +@pytest.fixture(name="client") +async def set_up_fixture( + hass: HomeAssistant, + aiohttp_client: ClientSessionGenerator, + app_strict_connection: web.Application, + cloud: MagicMock, + socket_enabled: None, +) -> TestClient: + """Set up the fixture.""" + + await async_setup_auth(hass, app_strict_connection, StrictConnectionMode.DISABLED) + assert await async_setup_component(hass, "cloud", {"cloud": {}}) + await hass.async_block_till_done() + return await aiohttp_client(app_strict_connection) + + +@pytest.mark.parametrize( + "strict_connection_mode", [e.value for e in StrictConnectionMode] +) +async def test_strict_connection_cloud_authenticated_requests( + hass: HomeAssistant, + client: TestClient, + hass_access_token: str, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + refresh_token: RefreshToken, + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test authenticated requests with strict connection.""" + assert hass.auth.session._strict_connection_sessions == {} + + signed_path = async_sign_path( + hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + + await set_cloud_prefs( + { + PREF_STRICT_CONNECTION: strict_connection_mode, + } + ) + + with simulate_cloud_request(): + assert is_cloud_connection(hass) + req = await client.get( + "/", headers={"Authorization": f"Bearer {hass_access_token}"} + ) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + + +async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests( + hass: HomeAssistant, + client: TestClient, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + _: RefreshToken, +) -> None: + """Test external unauthenticated requests with strict connection cloud enabled.""" + with simulate_cloud_request(): + assert is_cloud_connection(hass) + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests_refresh_token( + hass: HomeAssistant, + client: TestClient, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + refresh_token: RefreshToken, +) -> None: + """Test external unauthenticated requests with strict connection cloud enabled and refresh token cookie.""" + session = hass.auth.session + + # set strict connection cookie with refresh token + session_id = await _modify_cookie_for_cloud(client, "refresh") + assert session._strict_connection_sessions == {session_id: refresh_token.id} + with simulate_cloud_request(): + assert is_cloud_connection(hass) + req = await client.get("/") + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": False} + + # Invalidate refresh token, which should also invalidate session + hass.auth.async_remove_refresh_token(refresh_token) + assert session._strict_connection_sessions == {} + + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests_temp_session( + hass: HomeAssistant, + client: TestClient, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + _: RefreshToken, +) -> None: + """Test external unauthenticated requests with strict connection cloud enabled and temp cookie.""" + session = hass.auth.session + + # set strict connection cookie with temp session + assert session._temp_sessions == {} + session_id = await _modify_cookie_for_cloud(client, "temp") + assert session_id in session._temp_sessions + with simulate_cloud_request(): + assert is_cloud_connection(hass) + resp = await client.get("/") + assert resp.status == HTTPStatus.OK + assert await resp.json() == {"authenticated": False} + + async_fire_time_changed(hass, utcnow() + TEMP_TIMEOUT + timedelta(minutes=1)) + await hass.async_block_till_done(wait_background_tasks=True) + assert session._temp_sessions == {} + + await perform_unauthenticated_request(hass, client) + + +async def _drop_connection_unauthorized_request( + _: HomeAssistant, client: TestClient +) -> None: + with pytest.raises(ServerDisconnectedError): + # unauthorized requests should raise ServerDisconnectedError + await client.get("/") + + +async def _guard_page_unauthorized_request( + hass: HomeAssistant, client: TestClient +) -> None: + req = await client.get("/") + assert req.status == HTTPStatus.IM_A_TEAPOT + + def read_guard_page() -> str: + with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file: + return file.read() + + assert await req.text() == await hass.async_add_executor_job(read_guard_page) + + +@pytest.mark.skip(reason="Remove strict connection config option") +@pytest.mark.parametrize( + "test_func", + [ + _test_strict_connection_cloud_enabled_external_unauthenticated_requests, + _test_strict_connection_cloud_enabled_external_unauthenticated_requests_refresh_token, + _test_strict_connection_cloud_enabled_external_unauthenticated_requests_temp_session, + ], + ids=[ + "no cookie", + "refresh token cookie", + "temp session cookie", + ], +) +@pytest.mark.parametrize( + ("strict_connection_mode", "request_func"), + [ + (StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request), + (StrictConnectionMode.GUARD_PAGE, _guard_page_unauthorized_request), + ], + ids=["drop connection", "static page"], +) +async def test_strict_connection_cloud_external_unauthenticated_requests( + hass: HomeAssistant, + client: TestClient, + refresh_token: RefreshToken, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + test_func: Callable[ + [ + HomeAssistant, + TestClient, + Callable[[HomeAssistant, TestClient], Awaitable[None]], + RefreshToken, + ], + Awaitable[None], + ], + strict_connection_mode: StrictConnectionMode, + request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]], +) -> None: + """Test external unauthenticated requests with strict connection cloud.""" + await set_cloud_prefs( + { + PREF_STRICT_CONNECTION: strict_connection_mode, + } + ) + + await test_func( + hass, + client, + request_func, + refresh_token, + ) + + +async def _modify_cookie_for_cloud(client: TestClient, token_type: str) -> str: + """Modify cookie for cloud.""" + # Cloud cookie has set secure=true and will not set on unsecure connection + # As we test with unsecure connection, we need to set it manually + # We get the session via http and modify the cookie name to the secure one + session_id = await (await client.get(f"/test/cookie?token={token_type}")).text() + cookie_jar = client.session.cookie_jar + localhost = URL("http://127.0.0.1") + cookie = cookie_jar.filter_cookies(localhost)[COOKIE_NAME].value + assert cookie + cookie_jar.clear() + cookie_jar.update_cookies({PREFIXED_COOKIE_NAME: cookie}, localhost) + return session_id diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 142eab621e5..4b0df91bc60 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -25,7 +25,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -35,7 +35,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone" assert result["errors"] == {} @@ -45,7 +45,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "records" assert result["errors"] is None @@ -56,7 +56,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_ZONE[CONF_ZONE] assert result["data"] @@ -84,7 +84,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant, cfupdate_flow) -> N USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -102,7 +102,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant, cfupdate_flow) -> Non USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -122,7 +122,7 @@ async def test_user_form_unexpected_exception( USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -136,7 +136,7 @@ async def test_user_form_single_instance_allowed(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -154,7 +154,7 @@ async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: }, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with _patch_async_setup_entry() as mock_setup_entry: @@ -164,7 +164,7 @@ async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_TOKEN] == "other_token" diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index e3bf9e3c818..7397b6e2355 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form_home(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -42,7 +42,7 @@ async def test_form_home(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "CO2 Signal" assert result2["data"] == { "api_key": "api_key", @@ -57,7 +57,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -67,7 +67,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.co2signal.async_setup_entry", @@ -82,7 +82,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "12.3, 45.6" assert result3["data"] == { "latitude": 12.3, @@ -99,7 +99,7 @@ async def test_form_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -109,7 +109,7 @@ async def test_form_country(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.co2signal.async_setup_entry", @@ -123,7 +123,7 @@ async def test_form_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "fr" assert result3["data"] == { "country_code": "fr", @@ -167,7 +167,7 @@ async def test_form_error_handling( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": err_code} # reset mock and test if now succeeds @@ -183,7 +183,7 @@ async def test_form_error_handling( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "CO2 Signal" assert result["data"] == { "api_key": "api_key", @@ -207,7 +207,7 @@ async def test_reauth( data=None, ) - assert init_result["type"] == FlowResultType.FORM + assert init_result["type"] is FlowResultType.FORM assert init_result["step_id"] == "reauth" with patch( @@ -222,6 +222,6 @@ async def test_reauth( ) await hass.async_block_till_done() - assert configure_result["type"] == FlowResultType.ABORT + assert configure_result["type"] is FlowResultType.ABORT assert configure_result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index 79b0115bc7c..f213392bb1e 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components.coinbase.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ( init_mock_coinbase, @@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test User" assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"} assert len(mock_setup_entry.mock_calls) == 1 @@ -95,7 +96,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert "Coinbase rejected API credentials due to an unknown error" in caplog.text @@ -117,7 +118,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth_key"} assert "Coinbase rejected API credentials due to an invalid API key" in caplog.text @@ -139,7 +140,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth_secret"} assert ( "Coinbase rejected API credentials due to an invalid API secret" in caplog.text @@ -164,7 +165,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -186,7 +187,7 @@ async def test_form_catch_all_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -219,7 +220,7 @@ async def test_option_form(hass: HomeAssistant) -> None: CONF_EXCHANGE_PRECISION: 5, }, ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert len(mock_update_listener.mock_calls) == 1 @@ -249,7 +250,7 @@ async def test_form_bad_account_currency(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "currency_unavailable"} @@ -277,7 +278,7 @@ async def test_form_bad_exchange_rate(hass: HomeAssistant) -> None: CONF_EXCHANGE_PRECISION: 5, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "exchange_rate_unavailable"} @@ -311,5 +312,5 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index 5af762f557a..99b6bb4a9bd 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -2,13 +2,13 @@ from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components.coinbase.const import ( API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_RATES, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -45,12 +45,12 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entry = await init_mock_coinbase(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/color_extractor/test_config_flow.py b/tests/components/color_extractor/test_config_flow.py index 844712f1938..972b78b3f59 100644 --- a/tests/components/color_extractor/test_config_flow.py +++ b/tests/components/color_extractor/test_config_flow.py @@ -2,10 +2,8 @@ from unittest.mock import patch -import pytest - from homeassistant.components.color_extractor.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -18,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -30,42 +28,22 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Color extractor" assert result.get("data") == {} assert result.get("options") == {} assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) -async def test_single_instance_allowed( - hass: HomeAssistant, - source: str, -) -> None: +async def test_single_instance_allowed(hass: HomeAssistant) -> 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}, data={} + DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" - - -async def test_import_flow( - hass: HomeAssistant, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={}, - ) - - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("title") == "Color extractor" - assert result.get("data") == {} - assert result.get("options") == {} diff --git a/tests/components/color_extractor/test_init.py b/tests/components/color_extractor/test_init.py deleted file mode 100644 index cf4354db48d..00000000000 --- a/tests/components/color_extractor/test_init.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test Color extractor component setup process.""" - -from homeassistant.components.color_extractor import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component - - -async def test_legacy_migration(hass: HomeAssistant) -> None: - """Test migration from yaml to config flow.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - 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 diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index 851e179dd95..333bf09bd20 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -40,13 +40,13 @@ async def test_full_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == user_input[CONF_HOST] assert result["data"][CONF_PORT] == user_input[CONF_PORT] assert result["data"][CONF_PIN] == user_input[CONF_PIN] @@ -70,7 +70,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with ( @@ -89,7 +89,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result["flow_id"], user_input=MOCK_USER_BRIDGE_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is not None assert result["errors"]["base"] == error @@ -119,7 +119,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -130,7 +130,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -161,7 +161,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -171,7 +171,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is not None assert result["errors"]["base"] == error diff --git a/tests/components/command_line/__init__.py b/tests/components/command_line/__init__.py index 736ca68b43d..dc965234506 100644 --- a/tests/components/command_line/__init__.py +++ b/tests/components/command_line/__init__.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch @contextmanager def mock_asyncio_subprocess_run( - response: bytes = b"", returncode: int = 0, exception: Exception = None + response: bytes = b"", returncode: int = 0, exception: Exception | None = None ): """Mock create_subprocess_shell.""" diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 8b98d8d1623..7ed48909d79 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -383,3 +383,48 @@ async def test_availability( entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == STATE_UNAVAILABLE + + +async def test_icon_template(hass: HomeAssistant) -> None: + """Test with state value.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, "cover_status_icon") + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "cover": { + "command_state": f"cat {path}", + "command_open": f"echo 100 > {path}", + "command_close": f"echo 0 > {path}", + "command_stop": f"echo 0 > {path}", + "name": "Test", + "icon": "{% if this.state=='open' %} mdi:open {% else %} mdi:closed {% endif %}", + } + } + ] + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, + ) + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.attributes.get("icon") == "mdi:closed" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, + ) + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.attributes.get("icon") == "mdi:open" diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 80f68b96fe1..b17face10d9 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -10,7 +10,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.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import yaml @@ -82,10 +82,8 @@ async def test_update_automation_config( ) await hass.async_block_till_done() assert sorted(hass.states.async_entity_ids("automation")) == [ - "automation.automation_0", "automation.automation_1", ] - assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE assert hass.states.get("automation.automation_1").state == STATE_ON assert resp.status == HTTPStatus.OK @@ -260,10 +258,8 @@ async def test_update_remove_key_automation_config( ) await hass.async_block_till_done() assert sorted(hass.states.async_entity_ids("automation")) == [ - "automation.automation_0", "automation.automation_1", ] - assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE assert hass.states.get("automation.automation_1").state == STATE_ON assert resp.status == HTTPStatus.OK @@ -305,10 +301,8 @@ async def test_bad_formatted_automations( ) await hass.async_block_till_done() assert sorted(hass.states.async_entity_ids("automation")) == [ - "automation.automation_0", "automation.automation_1", ] - assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE assert hass.states.get("automation.automation_1").state == STATE_ON assert resp.status == HTTPStatus.OK diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index b4ef32b864c..87c712b3716 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -13,6 +13,7 @@ from homeassistant.components.config import config_entries from homeassistant.config_entries import HANDLERS, ConfigFlow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.loader import IntegrationNotFound from homeassistant.setup import async_setup_component @@ -131,6 +132,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp2", @@ -145,6 +148,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp3", @@ -159,6 +164,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": core_ce.ConfigEntryDisabler.USER, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp4", @@ -173,6 +180,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp5", @@ -187,6 +196,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, ] @@ -240,6 +251,7 @@ async def test_reload_entry(hass: HomeAssistant, client) -> None: domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED ) entry.add_to_hass(hass) + hass.config.components.add("kitchen_sink") resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) @@ -287,6 +299,7 @@ async def test_reload_entry_in_failed_state( """Test reloading an entry via the API that has already failed to unload.""" entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) entry.add_to_hass(hass) + hass.config.components.add("demo") resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) @@ -315,6 +328,7 @@ async def test_reload_entry_in_setup_retry( entry = MockConfigEntry(domain="comp", state=core_ce.ConfigEntryState.SETUP_RETRY) entry.supports_unload = True entry.add_to_hass(hass) + hass.config.components.add("comp") with patch.dict(HANDLERS, {"comp": ConfigFlow, "test": ConfigFlow}): resp = await client.post( @@ -536,6 +550,8 @@ async def test_create_account( "pref_disable_polling": False, "title": "Test Entry", "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, "description": None, "description_placeholders": None, @@ -615,6 +631,8 @@ async def test_two_step_flow( "pref_disable_polling": False, "title": "user-title", "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, "description": None, "description_placeholders": None, @@ -1058,6 +1076,8 @@ async def test_get_single( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "user", "state": "loaded", "supports_reconfigure": False, @@ -1092,6 +1112,7 @@ async def test_update_prefrences( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED ) entry.add_to_hass(hass) + hass.config.components.add("kitchen_sink") assert entry.pref_disable_new_entities is False assert entry.pref_disable_polling is False @@ -1192,6 +1213,7 @@ async def test_disable_entry( ) entry.add_to_hass(hass) assert entry.disabled_by is None + hass.config.components.add("kitchen_sink") # Disable await ws_client.send_json( @@ -1289,7 +1311,7 @@ async def test_ignore_flow( result = await hass.config_entries.flow.async_init( "test", context={"source": core_ce.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await ws_client.send_json( { @@ -1393,6 +1415,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1408,6 +1432,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla2", "state": "setup_error", "supports_reconfigure": False, @@ -1423,6 +1449,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1438,6 +1466,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla4", "state": "not_loaded", "supports_reconfigure": False, @@ -1453,6 +1483,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla5", "state": "not_loaded", "supports_reconfigure": False, @@ -1479,6 +1511,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1504,6 +1538,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla4", "state": "not_loaded", "supports_reconfigure": False, @@ -1519,6 +1555,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla5", "state": "not_loaded", "supports_reconfigure": False, @@ -1544,6 +1582,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1559,6 +1599,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1590,6 +1632,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1605,6 +1649,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla2", "state": "setup_error", "supports_reconfigure": False, @@ -1620,6 +1666,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1635,6 +1683,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla4", "state": "not_loaded", "supports_reconfigure": False, @@ -1650,6 +1700,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla5", "state": "not_loaded", "supports_reconfigure": False, @@ -1749,6 +1801,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1767,6 +1821,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla2", "state": "setup_error", "supports_reconfigure": False, @@ -1785,6 +1841,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1807,6 +1865,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1830,6 +1890,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1853,6 +1915,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1935,6 +1999,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1953,6 +2019,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1977,6 +2045,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1999,6 +2069,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -2023,6 +2095,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -2046,6 +2120,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -2225,6 +2301,8 @@ async def test_supports_reconfigure( "pref_disable_polling": False, "title": "Test Entry", "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, "description": None, "description_placeholders": None, diff --git a/tests/components/configurator/test_init.py b/tests/components/configurator/test_init.py index 44813f01a18..6c937473ddc 100644 --- a/tests/components/configurator/test_init.py +++ b/tests/components/configurator/test_init.py @@ -2,7 +2,7 @@ from datetime import timedelta -import homeassistant.components.configurator as configurator +from homeassistant.components import configurator from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util diff --git a/tests/components/conftest.py b/tests/components/conftest.py index d84fb3600ab..bde8cad5ea4 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,16 +1,21 @@ """Fixtures for component testing.""" -from collections.abc import Generator +from collections.abc import Callable, Generator from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from tests.components.conversation import MockAgent if TYPE_CHECKING: + from tests.components.device_tracker.common import MockScanner from tests.components.light.common import MockLight from tests.components.sensor.common import MockSensor + from tests.components.switch.common import MockSwitch @pytest.fixture(scope="session", autouse=True) @@ -100,6 +105,16 @@ def tts_mutagen_mock_fixture(): yield from tts_mutagen_mock_fixture_helper() +@pytest.fixture(name="mock_conversation_agent") +def mock_conversation_agent_fixture(hass: HomeAssistant) -> MockAgent: + """Mock a conversation agent.""" + from tests.components.conversation.common import ( + mock_conversation_agent_fixture_helper, + ) + + return mock_conversation_agent_fixture_helper(hass) + + @pytest.fixture(scope="session", autouse=True) def prevent_ffmpeg_subprocess() -> Generator[None, None, None]: """Prevent ffmpeg from creating a subprocess.""" @@ -127,3 +142,29 @@ def mock_sensor_entities() -> dict[str, "MockSensor"]: from tests.components.sensor.common import get_mock_sensor_entities return get_mock_sensor_entities() + + +@pytest.fixture +def mock_switch_entities() -> list["MockSwitch"]: + """Return mocked toggle entities.""" + from tests.components.switch.common import get_mock_switch_entities + + return get_mock_switch_entities() + + +@pytest.fixture +def mock_legacy_device_scanner() -> "MockScanner": + """Return mocked legacy device scanner entity.""" + from tests.components.device_tracker.common import MockScanner + + return MockScanner() + + +@pytest.fixture +def mock_legacy_device_tracker_setup() -> ( + Callable[[HomeAssistant, "MockScanner"], None] +): + """Return setup callable for legacy device tracker setup.""" + from tests.components.device_tracker.common import mock_legacy_device_tracker_setup + + return mock_legacy_device_tracker_setup diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 8ec6df063e5..d1faf2da6c6 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -48,7 +49,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} c4_account = _get_mock_c4_account() @@ -77,7 +78,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "control4_model_00AA00AA00AA" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -107,7 +108,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -130,7 +131,7 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -159,7 +160,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -170,14 +171,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 4}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: 4, } @@ -190,13 +191,13 @@ async def test_option_flow_defaults(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, } diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index 7209148e21f..fb9bcab7498 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -5,11 +5,16 @@ from __future__ import annotations from typing import Literal from homeassistant.components import conversation +from homeassistant.components.conversation.models import ( + ConversationInput, + ConversationResult, +) from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, ExposedEntities, async_expose_entity, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import intent @@ -30,24 +35,22 @@ class MockAgent(conversation.AbstractConversationAgent): """Return a list of supported languages.""" return self._supported_languages - async def async_process( - self, user_input: conversation.ConversationInput - ) -> conversation.ConversationResult: + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process some text.""" self.calls.append(user_input) response = intent.IntentResponse(language=user_input.language) response.async_set_speech(self.response) - return conversation.ConversationResult( + return ConversationResult( response=response, conversation_id=user_input.conversation_id ) -def expose_new(hass, expose_new): +def expose_new(hass: HomeAssistant, expose_new: bool): """Enable exposing new entities to the default agent.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) -def expose_entity(hass, entity_id, should_expose): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool): """Expose an entity to the default agent.""" async_expose_entity(hass, conversation.DOMAIN, entity_id, should_expose) diff --git a/tests/components/conversation/common.py b/tests/components/conversation/common.py new file mode 100644 index 00000000000..2fa152b1eb2 --- /dev/null +++ b/tests/components/conversation/common.py @@ -0,0 +1,17 @@ +"""Provide common tests tools for conversation.""" + +from homeassistant.components import conversation +from homeassistant.core import HomeAssistant + +from . import MockAgent + +from tests.common import MockConfigEntry + + +def mock_conversation_agent_fixture_helper(hass: HomeAssistant) -> MockAgent: + """Mock agent.""" + entry = MockConfigEntry(entry_id="mock-entry") + entry.add_to_hass(hass) + agent = MockAgent(entry.entry_id, ["smurfish"]) + conversation.async_set_agent(hass, entry, agent) + return agent diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index cf6b4567228..4801e506460 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -7,6 +7,8 @@ import pytest from homeassistant.components import conversation from homeassistant.components.shopping_list import intent as sl_intent from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from . import MockAgent @@ -14,17 +16,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_agent(hass): - """Mock agent.""" - entry = MockConfigEntry(entry_id="mock-entry") - entry.add_to_hass(hass) - agent = MockAgent(entry.entry_id, ["smurfish"]) - conversation.async_set_agent(hass, entry, agent) - return agent - - -@pytest.fixture -def mock_agent_support_all(hass): +def mock_agent_support_all(hass: HomeAssistant): """Mock agent that supports all languages.""" entry = MockConfigEntry(entry_id="mock-entry-support-all") entry.add_to_hass(hass) @@ -44,7 +36,7 @@ def mock_shopping_list_io(): @pytest.fixture -async def sl_setup(hass): +async def sl_setup(hass: HomeAssistant): """Set up the shopping list.""" entry = MockConfigEntry(domain="shopping_list") @@ -53,3 +45,10 @@ async def sl_setup(hass): assert await hass.config_entries.async_setup(entry.entry_id) await sl_intent.async_setup_intents(hass) + + +@pytest.fixture +async def init_components(hass: HomeAssistant): + """Initialize relevant components with empty configs.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 6af9d197e01..d514d145477 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -101,7 +101,7 @@ # --- # name: test_get_agent_info dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', }) # --- @@ -113,7 +113,7 @@ # --- # name: test_get_agent_info.2 dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', }) # --- @@ -127,7 +127,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'af', @@ -207,7 +207,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ ]), @@ -231,7 +231,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'en', @@ -255,7 +255,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'en', @@ -279,7 +279,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'de', @@ -304,7 +304,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'de-CH', @@ -415,6 +415,36 @@ }), }) # --- +# name: test_http_processing_intent[conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': 'entity', + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_http_processing_intent[homeassistant] dict({ 'conversation_id': None, @@ -1035,6 +1065,36 @@ }), }) # --- +# name: test_turn_on_intent[None-turn kitchen on-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[None-turn kitchen on-homeassistant] dict({ 'conversation_id': None, @@ -1095,6 +1155,36 @@ }), }) # --- +# name: test_turn_on_intent[None-turn on kitchen-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[None-turn on kitchen-homeassistant] dict({ 'conversation_id': None, @@ -1155,6 +1245,36 @@ }), }) # --- +# name: test_turn_on_intent[my_new_conversation-turn kitchen on-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[my_new_conversation-turn kitchen on-homeassistant] dict({ 'conversation_id': None, @@ -1215,6 +1335,36 @@ }), }) # --- +# name: test_turn_on_intent[my_new_conversation-turn on kitchen-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[my_new_conversation-turn on kitchen-homeassistant] dict({ 'conversation_id': None, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 8f38459a8da..9048a1259c5 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -7,6 +7,7 @@ from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult import pytest from homeassistant.components import conversation +from homeassistant.components.conversation import default_agent from homeassistant.components.homeassistant.exposed_entities import ( async_get_assistant_settings, ) @@ -151,9 +152,7 @@ async def test_conversation_agent( init_components, ) -> None: """Test DefaultAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( - conversation.HOME_ASSISTANT_AGENT - ) + agent = default_agent.async_get_default_agent(hass) with patch( "homeassistant.components.conversation.default_agent.get_languages", return_value=["dwarvish", "elvish", "entish"], @@ -180,6 +179,7 @@ async def test_expose_flag_automatically_set( # After setting up conversation, the expose flag should now be set on all entities assert async_get_assistant_settings(hass, conversation.DOMAIN) == { + "conversation.home_assistant": {"should_expose": False}, light.entity_id: {"should_expose": True}, test.entity_id: {"should_expose": False}, } @@ -189,6 +189,7 @@ async def test_expose_flag_automatically_set( hass.states.async_set(new_light, "test") await hass.async_block_till_done() assert async_get_assistant_settings(hass, conversation.DOMAIN) == { + "conversation.home_assistant": {"should_expose": False}, light.entity_id: {"should_expose": True}, new_light: {"should_expose": True}, test.entity_id: {"should_expose": False}, @@ -253,10 +254,8 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: trigger_sentences = ["It's party time", "It is time to party"] trigger_response = "Cowabunga!" - agent = await conversation._get_agent_manager(hass).async_get_agent( - conversation.HOME_ASSISTANT_AGENT - ) - assert isinstance(agent, conversation.DefaultAgent) + agent = default_agent.async_get_default_agent(hass) + assert isinstance(agent, default_agent.DefaultAgent) callback = AsyncMock(return_value=trigger_response) unregister = agent.register_trigger(trigger_sentences, callback) @@ -824,7 +823,7 @@ async def test_empty_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={" "}, floor_id=floor_1 + area_kitchen.id, aliases={" "}, floor_id=floor_1.floor_id ) entry = MockConfigEntry() @@ -850,7 +849,7 @@ async def test_empty_aliases( ) with patch( - "homeassistant.components.conversation.DefaultAgent._recognize", + "homeassistant.components.conversation.default_agent.DefaultAgent._recognize", return_value=None, ) as mock_recognize_all: await conversation.async_converse( diff --git a/tests/components/conversation/test_entity.py b/tests/components/conversation/test_entity.py new file mode 100644 index 00000000000..c84f94c4aa4 --- /dev/null +++ b/tests/components/conversation/test_entity.py @@ -0,0 +1,47 @@ +"""Tests for conversation entity.""" + +from unittest.mock import patch + +from homeassistant.core import Context, HomeAssistant, State +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import mock_restore_cache + + +async def test_state_set_and_restore(hass: HomeAssistant) -> None: + """Test we set and restore state in the integration.""" + entity_id = "conversation.home_assistant" + timestamp = "2023-01-01T23:59:59+00:00" + mock_restore_cache(hass, (State(entity_id, timestamp),)) + + await async_setup_component(hass, "homeassistant", {}) + await async_setup_component(hass, "conversation", {}) + + state = hass.states.get(entity_id) + assert state + assert state.state == timestamp + + now = dt_util.utcnow() + context = Context() + + with ( + patch( + "homeassistant.components.conversation.default_agent.DefaultAgent.async_process" + ) as mock_process, + patch("homeassistant.util.dt.utcnow", return_value=now), + ): + await hass.services.async_call( + "conversation", + "process", + {"text": "Hello"}, + context=context, + blocking=True, + ) + + assert len(mock_process.mock_calls) == 1 + + state = hass.states.get(entity_id) + assert state + assert state.state == now.isoformat() + assert state.context is context diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 1ef8c8b30d7..5b117c1ac70 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -9,6 +9,8 @@ from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.components import conversation +from homeassistant.components.conversation import default_agent +from homeassistant.components.conversation.models import ConversationInput from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME @@ -33,7 +35,13 @@ from tests.common import ( from tests.components.light.common import MockLight from tests.typing import ClientSessionGenerator, WebSocketGenerator -AGENT_ID_OPTIONS = [None, conversation.HOME_ASSISTANT_AGENT] +AGENT_ID_OPTIONS = [ + None, + # Old value of conversation.HOME_ASSISTANT_AGENT, + "homeassistant", + # Current value of conversation.HOME_ASSISTANT_AGENT, + "conversation.home_assistant", +] class OrderBeerIntentHandler(intent.IntentHandler): @@ -49,14 +57,6 @@ class OrderBeerIntentHandler(intent.IntentHandler): return response -@pytest.fixture -async def init_components(hass): - """Initialize relevant components with empty configs.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) - assert await async_setup_component(hass, "intent", {}) - - @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) async def test_http_processing_intent( hass: HomeAssistant, @@ -94,7 +94,7 @@ async def test_http_processing_intent_target_ha_agent( init_components, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, - mock_agent, + mock_conversation_agent, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -658,7 +658,7 @@ async def test_custom_agent( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, - mock_agent, + mock_conversation_agent, snapshot: SnapshotAssertion, ) -> None: """Test a custom conversation agent.""" @@ -672,7 +672,7 @@ async def test_custom_agent( "text": "Test Text", "conversation_id": "test-conv-id", "language": "test-language", - "agent_id": mock_agent.agent_id, + "agent_id": mock_conversation_agent.agent_id, } resp = await client.post("/api/conversation/process", json=data) @@ -683,14 +683,14 @@ async def test_custom_agent( assert data["response"]["speech"]["plain"]["speech"] == "Test response" assert data["conversation_id"] == "test-conv-id" - assert len(mock_agent.calls) == 1 - assert mock_agent.calls[0].text == "Test Text" - assert mock_agent.calls[0].context.user_id == hass_admin_user.id - assert mock_agent.calls[0].conversation_id == "test-conv-id" - assert mock_agent.calls[0].language == "test-language" + assert len(mock_conversation_agent.calls) == 1 + assert mock_conversation_agent.calls[0].text == "Test Text" + assert mock_conversation_agent.calls[0].context.user_id == hass_admin_user.id + assert mock_conversation_agent.calls[0].conversation_id == "test-conv-id" + assert mock_conversation_agent.calls[0].language == "test-language" conversation.async_unset_agent( - hass, hass.config_entries.async_get_entry(mock_agent.agent_id) + hass, hass.config_entries.async_get_entry(mock_conversation_agent.agent_id) ) @@ -750,8 +750,8 @@ async def test_ws_prepare( """Test the Websocket prepare conversation API.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = default_agent.async_get_default_agent(hass) + assert isinstance(agent, default_agent.DefaultAgent) # No intents should be loaded yet assert not agent._lang_intents.get(hass.config.language) @@ -852,8 +852,8 @@ async def test_prepare_reload(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) # Load intents - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = default_agent.async_get_default_agent(hass) + assert isinstance(agent, default_agent.DefaultAgent) await agent.async_prepare(language) # Confirm intents are loaded @@ -880,8 +880,8 @@ async def test_prepare_fail(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) # Load intents - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = default_agent.async_get_default_agent(hass) + assert isinstance(agent, default_agent.DefaultAgent) await agent.async_prepare("not-a-language") # Confirm no intents were loaded @@ -917,11 +917,11 @@ async def test_non_default_response(hass: HomeAssistant, init_components) -> Non hass.states.async_set("cover.front_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = default_agent.async_get_default_agent(hass) + assert isinstance(agent, default_agent.DefaultAgent) result = await agent.async_process( - conversation.ConversationInput( + ConversationInput( text="open the front door", context=Context(), conversation_id=None, @@ -1061,18 +1061,21 @@ async def test_light_area_same_name( assert call.data == {"entity_id": [kitchen_light.entity_id]} -async def test_agent_id_validator_invalid_agent(hass: HomeAssistant) -> None: +async def test_agent_id_validator_invalid_agent( + hass: HomeAssistant, init_components +) -> None: """Test validating agent id.""" with pytest.raises(vol.Invalid): conversation.agent_id_validator("invalid_agent") conversation.agent_id_validator(conversation.HOME_ASSISTANT_AGENT) + conversation.agent_id_validator("conversation.home_assistant") async def test_get_agent_list( hass: HomeAssistant, init_components, - mock_agent, + mock_conversation_agent, mock_agent_support_all, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, @@ -1128,14 +1131,20 @@ async def test_get_agent_list( async def test_get_agent_info( - hass: HomeAssistant, init_components, mock_agent, snapshot: SnapshotAssertion + hass: HomeAssistant, + init_components, + mock_conversation_agent, + snapshot: SnapshotAssertion, ) -> None: """Test get agent info.""" agent_info = conversation.async_get_agent_info(hass) # Test it's the default assert conversation.async_get_agent_info(hass, "homeassistant") == agent_info assert conversation.async_get_agent_info(hass, "homeassistant") == snapshot - assert conversation.async_get_agent_info(hass, mock_agent.agent_id) == snapshot + assert ( + conversation.async_get_agent_info(hass, mock_conversation_agent.agent_id) + == snapshot + ) assert conversation.async_get_agent_info(hass, "not exist") is None # Test the name when config entry title is empty diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 221789b49e0..83f4e97c853 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.components import conversation +from homeassistant.components.conversation import default_agent +from homeassistant.components.conversation.models import ConversationInput from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component @@ -103,6 +104,36 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None: assert service_response["response"]["speech"]["plain"]["speech"] == response +async def test_empty_response(hass: HomeAssistant, setup_comp) -> None: + """Test the conversation response action with an empty response.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": ["Open the pod bay door Hal"], + }, + "action": { + "set_conversation_response": "", + }, + } + }, + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + { + "text": "Open the pod bay door Hal", + }, + blocking=True, + return_response=True, + ) + assert service_response["response"]["speech"]["plain"]["speech"] == "" + + async def test_response_same_sentence(hass: HomeAssistant, calls, setup_comp) -> None: """Test the conversation response action with multiple triggers using the same sentence.""" assert await async_setup_component( @@ -514,11 +545,11 @@ async def test_trigger_with_device_id(hass: HomeAssistant) -> None: }, ) - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = default_agent.async_get_default_agent(hass) + assert isinstance(agent, default_agent.DefaultAgent) result = await agent.async_process( - conversation.ConversationInput( + ConversationInput( text="test sentence", context=Context(), conversation_id=None, diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py index 7ddf1fd5942..15670af4bc8 100644 --- a/tests/components/coolmaster/conftest.py +++ b/tests/components/coolmaster/conftest.py @@ -64,7 +64,7 @@ class CoolMasterNetUnitMock: self._attributes["mode"] = value return CoolMasterNetUnitMock(self.unit_id, self._attributes) - async def set_thermostat(self, value: int | float) -> CoolMasterNetUnitMock: + async def set_thermostat(self, value: float) -> CoolMasterNetUnitMock: """Set the target temperature.""" self._attributes["thermostat"] = value return CoolMasterNetUnitMock(self.unit_id, self._attributes) diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index ef7828e126d..83a074815b5 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant import config_entries from homeassistant.components.coolmaster.config_flow import AVAILABLE_MODES from homeassistant.components.coolmaster.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def _flow_data(): @@ -21,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -64,7 +65,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: result["flow_id"], _flow_data() ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -82,7 +83,7 @@ async def test_form_connection_refused(hass: HomeAssistant) -> None: result["flow_id"], _flow_data() ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -100,5 +101,5 @@ async def test_form_no_units(hass: HomeAssistant) -> None: result["flow_id"], _flow_data() ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_units"} diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 43bf7431626..e70e8d3a70f 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_PLATFORM, EntityCategory diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index a58f94f44f3..d1a542e6608 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 5db52b6d618..8e2f794f1e0 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( @@ -625,15 +625,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index ec090b878f2..5ccd948cc6b 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -4,7 +4,7 @@ from enum import Enum import pytest -import homeassistant.components.cover as cover +from homeassistant.components import cover from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, @@ -108,6 +108,15 @@ async def test_services( await call_service(hass, SERVICE_TOGGLE, ent6) assert is_opening(hass, ent6) + # After the unusual state transition: closing -> fully open, toggle should close + set_state(ent5, STATE_OPEN) + await call_service(hass, SERVICE_TOGGLE, ent5) # Start closing + assert is_closing(hass, ent5) + set_state(ent5, STATE_OPEN) # Unusual state transition from closing -> fully open + set_cover_position(ent5, 100) + await call_service(hass, SERVICE_TOGGLE, ent5) # Should close, not open + assert is_closing(hass, ent5) + def call_service(hass, service, ent): """Call any service on entity.""" diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 323eb80d712..0ebb8aede49 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -20,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "CPU Speed" assert result2.get("data") == {} @@ -49,7 +49,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -66,7 +66,7 @@ async def test_not_compatible( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_cpuinfo_config_flow.return_value = {} @@ -75,7 +75,7 @@ async def test_not_compatible( user_input={}, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "not_compatible" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index 04f69f3a74a..3525d8c3f53 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Generator -from typing import Union from unittest.mock import AsyncMock, MagicMock, patch from crownstone_cloud.cloud_models.spheres import Spheres @@ -14,7 +13,6 @@ from crownstone_cloud.exceptions import ( import pytest from serial.tools.list_ports_common import ListPortInfo -from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.crownstone.const import ( CONF_USB_MANUAL_PATH, @@ -28,10 +26,11 @@ from homeassistant.components.crownstone.const import ( ) 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 -MockFixture = Generator[Union[MagicMock, AsyncMock], None, None] +MockFixture = Generator[MagicMock | AsyncMock, None, None] @pytest.fixture(name="crownstone_setup") @@ -186,7 +185,7 @@ async def test_no_user_input( DOMAIN, context={"source": "user"} ) # show the login form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert crownstone_setup.call_count == 0 @@ -216,7 +215,7 @@ async def test_abort_if_configured( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # test if we abort if we try to configure the same entry - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert crownstone_setup.call_count == 0 @@ -233,7 +232,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} # side effect: auth error account not verified @@ -243,7 +242,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "account_not_verified"} assert crownstone_setup.call_count == 0 @@ -258,7 +257,7 @@ async def test_unknown_error( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown_error"} assert crownstone_setup.call_count == 0 @@ -278,14 +277,14 @@ async def test_successful_login_no_usb( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # should show usb form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" # don't setup USB dongle, create entry result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: DONT_USE_USB} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_without_usb assert result["options"] == entry_options_without_usb assert crownstone_setup.call_count == 1 @@ -311,7 +310,7 @@ async def test_successful_login_with_usb( hass, get_mocked_crownstone_cloud(create_mocked_spheres(2)) ) # should show usb form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports_none_types.call_count == 1 @@ -331,7 +330,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 @@ -340,7 +339,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_usb assert result["options"] == entry_options_with_usb assert crownstone_setup.call_count == 1 @@ -363,7 +362,7 @@ async def test_successful_login_with_manual_usb_path( hass, get_mocked_crownstone_cloud(create_mocked_spheres(1)) ) # should show usb form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -372,7 +371,7 @@ async def test_successful_login_with_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -384,7 +383,7 @@ async def test_successful_login_with_manual_usb_path( # since we only have 1 sphere here, test that it's automatically selected and # creating entry without asking for user input - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_manual_usb assert result["options"] == entry_options_with_manual_usb assert crownstone_setup.call_count == 1 @@ -420,7 +419,7 @@ async def test_options_flow_setup_usb( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -434,7 +433,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -454,7 +453,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports.call_count == 2 assert usb_path.call_count == 1 @@ -463,7 +462,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1" ) @@ -497,7 +496,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -514,7 +513,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant) -> None: CONF_USB_SPHERE_OPTION: "sphere_name_0", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=None, usb_sphere=None ) @@ -550,13 +549,13 @@ async def test_options_flow_manual_usb_path( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -565,7 +564,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -575,7 +574,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_MANUAL_PATH: path} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=path, usb_sphere="sphere_id_0" ) @@ -609,14 +608,14 @@ async def test_options_flow_change_usb_sphere(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True, CONF_USB_SPHERE_OPTION: "sphere_name_2"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_2" ) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index ece17b6aafe..6d957384d4d 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -51,7 +51,7 @@ async def test_user(hass: HomeAssistant, mock_daikin) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -59,7 +59,7 @@ async def test_user(hass: HomeAssistant, mock_daikin) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -74,7 +74,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, mock_daikin) -> None: data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -97,7 +97,7 @@ async def test_device_abort(hass: HomeAssistant, mock_daikin, s_effect, reason) context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["step_id"] == "user" @@ -109,7 +109,7 @@ async def test_api_password_abort(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "api_password"} assert result["step_id"] == "user" @@ -141,7 +141,7 @@ async def test_discovery_zeroconf( context={"source": source}, data=data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) @@ -151,7 +151,7 @@ async def test_discovery_zeroconf( data={CONF_HOST: HOST}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -160,5 +160,5 @@ async def test_discovery_zeroconf( data=data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index 01b21ebb6fd..d7d754dacd2 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -212,7 +212,7 @@ async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: @@ -228,4 +228,4 @@ async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 51d698186b7..36c1d951078 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -3,7 +3,7 @@ from unittest import mock from unittest.mock import patch -import homeassistant.components.datadog as datadog +from homeassistant.components import datadog from homeassistant.const import EVENT_LOGBOOK_ENTRY, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/date/common.py b/tests/components/date/common.py new file mode 100644 index 00000000000..38641ba63fb --- /dev/null +++ b/tests/components/date/common.py @@ -0,0 +1,20 @@ +"""Common helpers for date entity component tests.""" + +from datetime import date + +from homeassistant.components.date import DateEntity + +from tests.common import MockEntity + + +class MockDateEntity(MockEntity, DateEntity): + """Mock date class.""" + + @property + def native_value(self): + """Return the native value of this date.""" + return self._handle("native_value") + + def set_value(self, value: date) -> None: + """Change the date.""" + self._values["native_value"] = value diff --git a/tests/components/date/test_init.py b/tests/components/date/test_init.py index f0a0094f8b8..a6c517c7b9e 100644 --- a/tests/components/date/test_init.py +++ b/tests/components/date/test_init.py @@ -2,7 +2,7 @@ from datetime import date -from homeassistant.components.date import DOMAIN, SERVICE_SET_VALUE, DateEntity +from homeassistant.components.date import DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ( ATTR_DATE, ATTR_ENTITY_ID, @@ -12,25 +12,18 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component - -class MockDateEntity(DateEntity): - """Mock date device to use in tests.""" - - _attr_name = "date" - - def __init__(self, native_value=date(2020, 1, 1)) -> None: - """Initialize mock date entity.""" - self._attr_native_value = native_value - - async def async_set_value(self, value: date) -> None: - """Set the value of the date.""" - self._attr_native_value = value +from tests.common import setup_test_component_platform +from tests.components.date.common import MockDateEntity -async def test_date(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_date(hass: HomeAssistant) -> None: """Test date entity.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockDateEntity( + name="test", + unique_id="unique_date", + native_value=date(2020, 1, 1), + ) + setup_test_component_platform(hass, DOMAIN, [entity]) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/datetime/common.py b/tests/components/datetime/common.py new file mode 100644 index 00000000000..2a4ba542950 --- /dev/null +++ b/tests/components/datetime/common.py @@ -0,0 +1,20 @@ +"""Common helpers for the datetime entity component tests.""" + +from datetime import datetime + +from homeassistant.components.datetime import DateTimeEntity + +from tests.common import MockEntity + + +class MockDateTimeEntity(MockEntity, DateTimeEntity): + """Mock date/time class.""" + + @property + def native_value(self): + """Return the native value of this date/time.""" + return self._handle("native_value") + + def set_value(self, value: datetime) -> None: + """Change the time.""" + self._values["native_value"] = value diff --git a/tests/components/datetime/test_init.py b/tests/components/datetime/test_init.py index f85754f5e1f..da65e1bce9e 100644 --- a/tests/components/datetime/test_init.py +++ b/tests/components/datetime/test_init.py @@ -5,36 +5,31 @@ from zoneinfo import ZoneInfo import pytest -from homeassistant.components.datetime import ( - ATTR_DATETIME, - DOMAIN, - SERVICE_SET_VALUE, - DateTimeEntity, -) +from homeassistant.components.datetime import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import setup_test_component_platform +from tests.components.datetime.common import MockDateTimeEntity + DEFAULT_VALUE = datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC) -class MockDateTimeEntity(DateTimeEntity): - """Mock datetime device to use in tests.""" - - def __init__(self, native_value: datetime | None = DEFAULT_VALUE) -> None: - """Initialize mock datetime entity.""" - self._attr_native_value = native_value - - async def async_set_value(self, value: datetime) -> None: - """Change the date/time.""" - self._attr_native_value = value - - -async def test_datetime(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_datetime(hass: HomeAssistant) -> None: """Test date/time entity.""" hass.config.set_time_zone("UTC") - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, + DOMAIN, + [ + MockDateTimeEntity( + name="test", + unique_id="unique_datetime", + native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=UTC), + ) + ], + ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index ec926491724..c855076de2f 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -171,11 +171,11 @@ async def test_alarm_control_panel( # Event signals alarm control panel arming - for arming_event in { + for arming_event in ( AncillaryControlPanel.ARMING_AWAY, AncillaryControlPanel.ARMING_NIGHT, AncillaryControlPanel.ARMING_STAY, - }: + ): event_changed_sensor = { "t": "event", "e": "changed", @@ -190,10 +190,10 @@ async def test_alarm_control_panel( # Event signals alarm control panel pending - for pending_event in { + for pending_event in ( AncillaryControlPanel.ENTRY_DELAY, AncillaryControlPanel.EXIT_DELAY, - }: + ): event_changed_sensor = { "t": "event", "e": "changed", diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index a6910ef4b55..6da940e0918 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -57,14 +57,14 @@ async def test_flow_discovered_bridges( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -77,7 +77,7 @@ async def test_flow_discovered_bridges( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -104,7 +104,7 @@ async def test_flow_manual_configuration_decision( result["flow_id"], user_input={CONF_HOST: CONF_MANUAL_INPUT} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -112,7 +112,7 @@ async def test_flow_manual_configuration_decision( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -131,7 +131,7 @@ async def test_flow_manual_configuration_decision( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -155,7 +155,7 @@ async def test_flow_manual_configuration( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -163,7 +163,7 @@ async def test_flow_manual_configuration( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -182,7 +182,7 @@ async def test_flow_manual_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -201,7 +201,7 @@ async def test_manual_configuration_after_discovery_timeout( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -216,7 +216,7 @@ async def test_manual_configuration_after_discovery_ResponseError( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -237,7 +237,7 @@ async def test_manual_configuration_update_configuration( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -245,7 +245,7 @@ async def test_manual_configuration_update_configuration( user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -264,7 +264,7 @@ async def test_manual_configuration_update_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" @@ -285,7 +285,7 @@ async def test_manual_configuration_dont_update_configuration( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -293,7 +293,7 @@ async def test_manual_configuration_dont_update_configuration( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -312,7 +312,7 @@ async def test_manual_configuration_dont_update_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -330,7 +330,7 @@ async def test_manual_configuration_timeout_get_bridge( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -338,7 +338,7 @@ async def test_manual_configuration_timeout_get_bridge( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -353,7 +353,7 @@ async def test_manual_configuration_timeout_get_bridge( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_bridges" @@ -384,7 +384,7 @@ async def test_link_step_fails( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post("http://1.2.3.4:80/api", exc=raised_error) @@ -393,7 +393,7 @@ async def test_link_step_fails( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": error_string} @@ -410,7 +410,7 @@ async def test_reauth_flow_update_configuration( context={"source": SOURCE_REAUTH}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" new_api_key = "new_key" @@ -431,7 +431,7 @@ async def test_reauth_flow_update_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_API_KEY] == new_api_key @@ -454,7 +454,7 @@ async def test_flow_ssdp_discovery( context={"source": SOURCE_SSDP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" flows = hass.config_entries.flow.async_progress() @@ -471,7 +471,7 @@ async def test_flow_ssdp_discovery( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -505,7 +505,7 @@ async def test_ssdp_discovery_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -531,7 +531,7 @@ async def test_ssdp_discovery_dont_update_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -558,7 +558,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -581,7 +581,7 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: ), context={"source": SOURCE_HASSIO}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -600,7 +600,7 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { CONF_HOST: "mock-deconz", CONF_PORT: 80, @@ -636,7 +636,7 @@ async def test_hassio_discovery_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert config_entry.data[CONF_PORT] == 8080 @@ -666,7 +666,7 @@ async def test_hassio_discovery_dont_update_configuration( context={"source": SOURCE_HASSIO}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -678,7 +678,7 @@ async def test_option_flow( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "deconz_devices" result = await hass.config_entries.options.async_configure( @@ -690,7 +690,7 @@ async def test_option_flow( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False, diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 3f86182e032..655ae2f42e2 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -11,6 +11,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er from .test_gateway import ( @@ -186,7 +187,7 @@ async def test_number_entities( # Service set value beyond the supported range - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index 1e7cecd8850..37229d4a72e 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -62,7 +62,7 @@ async def test_flow_user(hass: HomeAssistant, api) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -80,7 +80,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant, api) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -89,7 +89,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -99,7 +99,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -123,13 +123,13 @@ async def test_flow_reauth(hass: HomeAssistant, api) -> None: data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 54eadc3bd91..b0536873d66 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,29 +1,43 @@ """The tests for the notify demo platform.""" -import logging +from collections.abc import Generator from unittest.mock import patch import pytest -import voluptuous as vol +from homeassistant.components import notify +from homeassistant.components.demo import DOMAIN import homeassistant.components.demo.notify as demo -import homeassistant.components.notify as notify -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.const import Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_capture_events - -CONFIG = {notify.DOMAIN: {"platform": "demo"}} - - -@pytest.fixture(autouse=True) -def autouse_disable_platforms(disable_platforms): - """Auto use the disable_platforms fixture.""" +from tests.common import MockConfigEntry, async_capture_events @pytest.fixture -def events(hass): +def notify_only() -> Generator[None, None]: + """Enable only the notify platform.""" + with patch( + "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", + [Platform.NOTIFY], + ): + yield + + +@pytest.fixture(autouse=True) +async def setup_notify(hass: HomeAssistant, notify_only: None) -> None: + """Initialize setup demo Notify entity.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("notify.notifier") + assert state is not None + + +@pytest.fixture +def events(hass: HomeAssistant) -> list[Event]: """Fixture that catches notify events.""" return async_capture_events(hass, demo.EVENT_NOTIFY) @@ -46,104 +60,26 @@ def record_calls(calls): return record_calls -@pytest.fixture(name="mock_demo_notify") -def mock_demo_notify_fixture(): - """Mock demo notify service.""" - with patch("homeassistant.components.demo.notify.get_service", autospec=True) as ns: - yield ns - - -async def setup_notify(hass): - """Test setup.""" - with assert_setup_component(1, notify.DOMAIN) as config: - assert await async_setup_component(hass, notify.DOMAIN, CONFIG) - assert config[notify.DOMAIN] - await hass.async_block_till_done() - - -async def test_no_notify_service( - hass: HomeAssistant, mock_demo_notify, caplog: pytest.LogCaptureFixture -) -> None: - """Test missing platform notify service instance.""" - caplog.set_level(logging.ERROR) - mock_demo_notify.return_value = None - await setup_notify(hass) - await hass.async_block_till_done() - assert mock_demo_notify.called - assert "Failed to initialize notification service demo" in caplog.text - - -async def test_discover_notify(hass: HomeAssistant, mock_demo_notify) -> None: - """Test discovery of notify demo platform.""" - assert notify.DOMAIN not in hass.config.components - mock_demo_notify.return_value = None - await discovery.async_load_platform( - hass, "notify", "demo", {"test_key": "test_val"}, {"notify": {}} - ) - await hass.async_block_till_done() - assert notify.DOMAIN in hass.config.components - assert mock_demo_notify.called - assert mock_demo_notify.mock_calls[0][1] == ( - hass, - {}, - {"test_key": "test_val"}, - ) - - -async def test_sending_none_message(hass: HomeAssistant, events) -> None: - """Test send with None as message.""" - await setup_notify(hass) - with pytest.raises(vol.Invalid): - await hass.services.async_call( - notify.DOMAIN, notify.SERVICE_NOTIFY, {notify.ATTR_MESSAGE: None} - ) - await hass.async_block_till_done() - assert len(events) == 0 - - -async def test_sending_templated_message(hass: HomeAssistant, events) -> None: - """Send a templated message.""" - await setup_notify(hass) - hass.states.async_set("sensor.temperature", 10) +async def test_sending_message(hass: HomeAssistant, events: list[Event]) -> None: + """Test sending a message.""" data = { - notify.ATTR_MESSAGE: "{{states.sensor.temperature.state}}", - notify.ATTR_TITLE: "{{ states.sensor.temperature.name }}", + "entity_id": "notify.notifier", + notify.ATTR_MESSAGE: "Test message", } - await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data) await hass.async_block_till_done() last_event = events[-1] - assert last_event.data[notify.ATTR_TITLE] == "temperature" - assert last_event.data[notify.ATTR_MESSAGE] == "10" + assert last_event.data[notify.ATTR_MESSAGE] == "Test message" -async def test_method_forwards_correct_data(hass: HomeAssistant, events) -> None: - """Test that all data from the service gets forwarded to service.""" - await setup_notify(hass) - data = { - notify.ATTR_MESSAGE: "my message", - notify.ATTR_TITLE: "my title", - notify.ATTR_DATA: {"hello": "world"}, - } - await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) - await hass.async_block_till_done() - assert len(events) == 1 - data = events[0].data - assert { - "message": "my message", - "title": "my title", - "data": {"hello": "world"}, - } == data - - -async def test_calling_notify_from_script_loaded_from_yaml_without_title( - hass: HomeAssistant, events +async def test_calling_notify_from_script_loaded_from_yaml( + hass: HomeAssistant, events: list[Event] ) -> None: """Test if we can call a notify from a script.""" - await setup_notify(hass) step = { - "service": "notify.notify", + "service": "notify.send_message", "data": { - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} + "entity_id": "notify.notifier", }, "data_template": {"message": "Test 123 {{ 2 + 2 }}\n"}, } @@ -155,63 +91,4 @@ async def test_calling_notify_from_script_loaded_from_yaml_without_title( assert len(events) == 1 assert { "message": "Test 123 4", - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}}, } == events[0].data - - -async def test_calling_notify_from_script_loaded_from_yaml_with_title( - hass: HomeAssistant, events -) -> None: - """Test if we can call a notify from a script.""" - await setup_notify(hass) - step = { - "service": "notify.notify", - "data": { - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} - }, - "data_template": {"message": "Test 123 {{ 2 + 2 }}\n", "title": "Test"}, - } - await async_setup_component( - hass, "script", {"script": {"test": {"sequence": step}}} - ) - await hass.services.async_call("script", "test") - await hass.async_block_till_done() - assert len(events) == 1 - assert { - "message": "Test 123 4", - "title": "Test", - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}}, - } == events[0].data - - -async def test_targets_are_services(hass: HomeAssistant) -> None: - """Test that all targets are exposed as individual services.""" - await setup_notify(hass) - assert hass.services.has_service("notify", "demo") is not None - service = "demo_test_target_name" - assert hass.services.has_service("notify", service) is not None - - -async def test_messages_to_targets_route( - hass: HomeAssistant, calls, record_calls -) -> None: - """Test message routing to specific target services.""" - await setup_notify(hass) - hass.bus.async_listen_once("notify", record_calls) - - await hass.services.async_call( - "notify", - "demo_test_target_name", - {"message": "my message", "title": "my title", "data": {"hello": "world"}}, - ) - - await hass.async_block_till_done() - - data = calls[0][0].data - - assert { - "message": "my message", - "target": ["test target id"], - "title": "my title", - "data": {"hello": "world"}, - } == data diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 3c41b98a3fa..20e3ce8fc11 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -16,6 +16,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component ENTITY_VOLUME = "number.volume" @@ -97,7 +98,7 @@ async def test_set_value_bad_range(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_VOLUME) assert state.state == "42.0" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index e2a82248fdf..bcab2fb3de0 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.components.remote import ATTR_COMMAND from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index e70f0144e6a..a3b982ab70e 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -219,7 +219,7 @@ async def test_services(hass: HomeAssistant) -> None: async def test_set_fan_speed(hass: HomeAssistant) -> None: """Test vacuum service to set the fan speed.""" - group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE, ENTITY_VACUUM_MOST]) + group_vacuums = f"{ENTITY_VACUUM_COMPLETE},{ENTITY_VACUUM_MOST}" old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) old_state_most = hass.states.get(ENTITY_VACUUM_MOST) @@ -239,7 +239,7 @@ async def test_set_fan_speed(hass: HomeAssistant) -> None: async def test_send_command(hass: HomeAssistant) -> None: """Test vacuum service to send a command.""" - group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE]) + group_vacuums = f"{ENTITY_VACUUM_COMPLETE}" old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) await common.async_send_command( diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 5f5a5c8f17c..324b795052c 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.denonavr.config_flow import ( CONF_MANUFACTURER, @@ -20,6 +20,7 @@ from homeassistant.components.denonavr.config_flow import ( ) from homeassistant.const import CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -91,7 +92,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -100,7 +101,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -121,7 +122,7 @@ async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -134,7 +135,7 @@ async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> Non {}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -155,7 +156,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -168,7 +169,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["errors"] == {} @@ -177,7 +178,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non {"select_host": TEST_HOST2}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST2, @@ -198,7 +199,7 @@ async def test_config_flow_manual_discover_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -211,7 +212,7 @@ async def test_config_flow_manual_discover_error(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} @@ -225,7 +226,7 @@ async def test_config_flow_manual_host_no_serial(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -238,7 +239,7 @@ async def test_config_flow_manual_host_no_serial(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -258,7 +259,7 @@ async def test_config_flow_manual_host_connection_error(hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -277,7 +278,7 @@ async def test_config_flow_manual_host_connection_error(hass: HomeAssistant) -> {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -290,7 +291,7 @@ async def test_config_flow_manual_host_no_device_info(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -303,7 +304,7 @@ async def test_config_flow_manual_host_no_device_info(hass: HomeAssistant) -> No {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -324,7 +325,7 @@ async def test_config_flow_ssdp(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -332,7 +333,7 @@ async def test_config_flow_ssdp(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -364,7 +365,7 @@ async def test_config_flow_ssdp_not_denon(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_denonavr_manufacturer" @@ -386,7 +387,7 @@ async def test_config_flow_ssdp_missing_info(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_denonavr_missing" @@ -410,7 +411,7 @@ async def test_config_flow_ssdp_ignored_model(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_denonavr_manufacturer" @@ -436,7 +437,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -449,7 +450,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, @@ -470,7 +471,7 @@ async def test_config_flow_manual_host_no_serial_double_config( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -483,7 +484,7 @@ async def test_config_flow_manual_host_no_serial_double_config( {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -497,7 +498,7 @@ async def test_config_flow_manual_host_no_serial_double_config( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -510,5 +511,5 @@ async def test_config_flow_manual_host_no_serial_double_config( {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 9002a201f85..3db0227c2a6 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My derivative" assert result["data"] == {} assert result["options"] == { @@ -96,7 +96,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -112,7 +112,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "unit_time": "h", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My derivative", "round": 2.0, diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index e4f57437d24..df050c58f10 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -74,7 +74,7 @@ async def setup_tests(hass, config, times, values, expected_state): # Testing a energy sensor with non-monotonic intervals and values base = dt_util.utcnow() with freeze_time(base) as freezer: - for time, value in zip(times, values): + for time, value in zip(times, values, strict=False): freezer.move_to(base + timedelta(seconds=time)) hass.states.async_set(entity_id, value, {}, force_update=True) await hass.async_block_till_done() @@ -175,7 +175,7 @@ async def test_data_moving_average_for_discrete_sensor(hass: HomeAssistant) -> N base = dt_util.utcnow() with freeze_time(base) as freezer: - for time, value in zip(times, temperature_values): + for time, value in zip(times, temperature_values, strict=False): now = base + timedelta(seconds=time) freezer.move_to(now) hass.states.async_set(entity_id, value, {}, force_update=True) @@ -219,7 +219,7 @@ async def test_data_moving_average_for_irregular_times(hass: HomeAssistant) -> N base = dt_util.utcnow() with freeze_time(base) as freezer: - for time, value in zip(times, temperature_values): + for time, value in zip(times, temperature_values, strict=False): now = base + timedelta(seconds=time) freezer.move_to(now) hass.states.async_set(entity_id, value, {}, force_update=True) @@ -257,7 +257,7 @@ async def test_double_signal_after_delay(hass: HomeAssistant) -> None: base = dt_util.utcnow() previous = 0 with freeze_time(base) as freezer: - for time, value in zip(times, temperature_values): + for time, value in zip(times, temperature_values, strict=False): now = base + timedelta(seconds=time) freezer.move_to(now) hass.states.async_set(entity_id, value, {}, force_update=True) diff --git a/tests/components/devialet/test_config_flow.py b/tests/components/devialet/test_config_flow.py index 05174b50f0d..6fec4e927dc 100644 --- a/tests/components/devialet/test_config_flow.py +++ b/tests/components/devialet/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect( @@ -49,7 +49,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +67,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -82,7 +82,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -94,7 +94,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] @@ -111,7 +111,7 @@ async def test_zeroconf_devialet( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.devialet.async_setup_entry", @@ -123,7 +123,7 @@ async def test_zeroconf_devialet( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Livingroom" assert result2["data"] == { CONF_HOST: HOST, @@ -140,7 +140,7 @@ async def test_async_step_confirm( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" aioclient_mock.get( @@ -150,6 +150,6 @@ async def test_async_step_confirm( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT.copy() ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 1a4488e43cd..3c3101d7a1f 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -7,14 +7,14 @@ import pytest from pytest_unordered import unordered import voluptuous as vol -from homeassistant import config_entries, loader -from homeassistant.components import device_automation -import homeassistant.components.automation as automation +from homeassistant import loader +from homeassistant.components import automation, device_automation from homeassistant.components.device_automation import ( InvalidDeviceAutomationConfig, toggle_entity, ) from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -328,23 +328,23 @@ async def test_websocket_get_action_capabilities( assert msg["success"] actions = msg["result"] - id = 2 + msg_id = 2 assert len(actions) == 3 for action in actions: await client.send_json( { - "id": id, + "id": msg_id, "type": "device_automation/action/capabilities", "action": action, } ) msg = await client.receive_json() - assert msg["id"] == id + assert msg["id"] == msg_id assert msg["type"] == TYPE_RESULT assert msg["success"] capabilities = msg["result"] assert capabilities == expected_capabilities[action["type"]] - id = id + 1 + msg_id = msg_id + 1 async def test_websocket_get_action_capabilities_unknown_domain( @@ -487,23 +487,23 @@ async def test_websocket_get_condition_capabilities( assert msg["success"] conditions = msg["result"] - id = 2 + msg_id = 2 assert len(conditions) == 2 for condition in conditions: await client.send_json( { - "id": id, + "id": msg_id, "type": "device_automation/condition/capabilities", "condition": condition, } ) msg = await client.receive_json() - assert msg["id"] == id + assert msg["id"] == msg_id assert msg["type"] == TYPE_RESULT assert msg["success"] capabilities = msg["result"] assert capabilities == expected_capabilities - id = id + 1 + msg_id = msg_id + 1 async def test_websocket_get_condition_capabilities_unknown_domain( @@ -775,23 +775,23 @@ async def test_websocket_get_trigger_capabilities( assert msg["success"] triggers = msg["result"] - id = 2 + msg_id = 2 assert len(triggers) == 3 # toggled, turned_on, turned_off for trigger in triggers: await client.send_json( { - "id": id, + "id": msg_id, "type": "device_automation/trigger/capabilities", "trigger": trigger, } ) msg = await client.receive_json() - assert msg["id"] == id + assert msg["id"] == msg_id assert msg["type"] == TYPE_RESULT assert msg["success"] capabilities = msg["result"] assert capabilities == expected_capabilities - id = id + 1 + msg_id = msg_id + 1 async def test_websocket_get_trigger_capabilities_unknown_domain( @@ -977,7 +977,7 @@ async def test_automation_with_dynamically_validated_action( module.async_validate_action_config = AsyncMock() config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1079,7 +1079,7 @@ async def test_automation_with_dynamically_validated_condition( module.async_validate_condition_config = AsyncMock() config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1193,7 +1193,7 @@ async def test_automation_with_dynamically_validated_trigger( module.async_validate_trigger_config = AsyncMock(wraps=lambda hass, config: config) config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1294,7 +1294,7 @@ async def test_automation_with_bad_action( ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1328,7 +1328,7 @@ async def test_automation_with_bad_condition_action( ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1361,7 +1361,7 @@ async def test_automation_with_bad_condition( ) -> None: """Test automation with bad device condition.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1446,8 +1446,10 @@ async def test_automation_with_sub_condition( "action": { "service": "test.automation", "data_template": { - "some": "and {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "and {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -1477,8 +1479,10 @@ async def test_automation_with_sub_condition( "action": { "service": "test.automation", "data_template": { - "some": "or {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "or {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -1526,7 +1530,7 @@ async def test_automation_with_bad_sub_condition( ) -> None: """Test automation with bad device condition under and/or conditions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1564,7 +1568,7 @@ async def test_automation_with_bad_trigger( ) -> None: """Test automation with bad device trigger.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/device_automation/test_toggle_entity.py b/tests/components/device_automation/test_toggle_entity.py index 59d316545fa..a8850bf50b9 100644 --- a/tests/components/device_automation/test_toggle_entity.py +++ b/tests/components/device_automation/test_toggle_entity.py @@ -4,7 +4,7 @@ from datetime import timedelta import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -64,15 +64,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -88,15 +85,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -112,15 +106,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -187,15 +178,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 570708cec79..5f44593aabe 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,5 +1,6 @@ """The tests device sun light trigger component.""" +from collections.abc import Callable from datetime import datetime from unittest.mock import patch @@ -21,25 +22,30 @@ from homeassistant.const import ( STATE_NOT_HOME, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) from homeassistant.core import CoreState, 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.device_tracker.common import MockScanner +from tests.components.light.common import MockLight @pytest.fixture -async def scanner(hass, enable_custom_integrations): +async def scanner( + hass: HomeAssistant, + mock_light_entities: list[MockLight], + mock_legacy_device_scanner: MockScanner, + mock_legacy_device_tracker_setup: Callable[[HomeAssistant, MockScanner], None], +) -> None: """Initialize components.""" - scanner = await getattr(hass.components, "test.device_tracker").async_get_scanner( - None, None - ) + mock_legacy_device_tracker_setup(hass, mock_legacy_device_scanner) + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("DEV1") - scanner.reset() - scanner.come_home("DEV1") - - getattr(hass.components, "test.light").init() + setup_test_component_platform(hass, "light", mock_light_entities) with patch( "homeassistant.components.device_tracker.legacy.load_yaml_config_file", @@ -145,10 +151,22 @@ async def test_lights_turn_on_when_coming_home_after_sun_set( hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} ) - hass.states.async_set(f"{DOMAIN}.device_2", STATE_HOME) - + hass.states.async_set(f"{DOMAIN}.device_2", STATE_UNKNOWN) await hass.async_block_till_done() + assert all( + hass.states.get(ent_id).state == STATE_OFF + for ent_id in hass.states.async_entity_ids("light") + ) + hass.states.async_set(f"{DOMAIN}.device_2", STATE_NOT_HOME) + await hass.async_block_till_done() + assert all( + hass.states.get(ent_id).state == STATE_OFF + for ent_id in hass.states.async_entity_ids("light") + ) + + hass.states.async_set(f"{DOMAIN}.device_2", STATE_HOME) + await hass.async_block_till_done() assert all( hass.states.get(ent_id).state == light.STATE_ON for ent_id in hass.states.async_entity_ids("light") diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index 973eb7d8820..a17556cfbaa 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -15,24 +15,29 @@ from homeassistant.components.device_tracker import ( ATTR_MAC, DOMAIN, SERVICE_SEE, + DeviceScanner, + ScannerEntity, + SourceType, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import GPSType from homeassistant.loader import bind_hass +from tests.common import MockPlatform, mock_platform + @callback @bind_hass def async_see( hass: HomeAssistant, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, gps_accuracy=None, - battery: int = None, - attributes: dict = None, + battery: int | None = None, + attributes: dict | None = None, ): """Call service to notify you see device.""" data = { @@ -51,3 +56,97 @@ def async_see( if attributes: data[ATTR_ATTRIBUTES] = attributes hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_SEE, data)) + + +class MockScannerEntity(ScannerEntity): + """Test implementation of a ScannerEntity.""" + + def __init__(self): + """Init.""" + self.connected = False + self._hostname = "test.hostname.org" + self._ip_address = "0.0.0.0" + self._mac_address = "ad:de:ef:be:ed:fe" + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SourceType.ROUTER + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + return 100 + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self._hostname + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self.connected + + def set_connected(self): + """Set connected to True.""" + self.connected = True + self.async_write_ha_state() + + +class MockScanner(DeviceScanner): + """Mock device scanner.""" + + def __init__(self): + """Initialize the MockScanner.""" + self.devices_home = [] + + def come_home(self, device): + """Make a device come home.""" + self.devices_home.append(device) + + def leave_home(self, device): + """Make a device leave the house.""" + self.devices_home.remove(device) + + def reset(self): + """Reset which devices are home.""" + self.devices_home = [] + + def scan_devices(self): + """Return a list of fake devices.""" + return list(self.devices_home) + + def get_device_name(self, device): + """Return a name for a mock device. + + Return None for dev1 for testing. + """ + return None if device == "DEV1" else device.lower() + + +def mock_legacy_device_tracker_setup( + hass: HomeAssistant, legacy_device_scanner: MockScanner +) -> None: + """Mock legacy device tracker platform setup.""" + + async def _async_get_scanner(hass, config) -> MockScanner: + """Return the test scanner.""" + return legacy_device_scanner + + mocked_platform = MockPlatform() + mocked_platform.async_get_scanner = _async_get_scanner + mock_platform(hass, "test.device_tracker", mocked_platform) diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index d8236c697c3..077e964f0af 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -336,14 +336,14 @@ async def test_load_unload_entry( ) -> None: """Test loading and unloading a config entry with a device tracker entity.""" config_entry = await create_mock_platform(hass, config_entry, [tracker_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert not state @@ -436,7 +436,7 @@ async def test_tracker_entity_state( ) -> None: """Test tracker entity state and state attributes.""" config_entry = await create_mock_platform(hass, config_entry, [tracker_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED hass.states.async_set( "zone.home", "0", @@ -482,7 +482,7 @@ async def test_scanner_entity_state( ) config_entry = await create_mock_platform(hass, config_entry, [scanner_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) assert entity_state @@ -581,88 +581,6 @@ def test_base_tracker_entity() -> None: assert entity.state_attributes is None -async def test_cleanup_legacy( - hass: HomeAssistant, - config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - device_registry: dr.DeviceRegistry, -) -> None: - """Test we clean up devices created by old device tracker.""" - device_entry_1 = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "device1")} - ) - device_entry_2 = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "device2")} - ) - device_entry_3 = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "device3")} - ) - - # Device with light + device tracker entity - entity_entry_1a = entity_registry.async_get_or_create( - DOMAIN, - "test", - "entity1a-unique", - config_entry=config_entry, - device_id=device_entry_1.id, - ) - entity_entry_1b = entity_registry.async_get_or_create( - "light", - "test", - "entity1b-unique", - config_entry=config_entry, - device_id=device_entry_1.id, - ) - # Just device tracker entity - entity_entry_2a = entity_registry.async_get_or_create( - DOMAIN, - "test", - "entity2a-unique", - config_entry=config_entry, - device_id=device_entry_2.id, - ) - # Device with no device tracker entities - entity_entry_3a = entity_registry.async_get_or_create( - "light", - "test", - "entity3a-unique", - config_entry=config_entry, - device_id=device_entry_3.id, - ) - # Device tracker but no device - entity_entry_4a = entity_registry.async_get_or_create( - DOMAIN, - "test", - "entity4a-unique", - config_entry=config_entry, - ) - # Completely different entity - entity_entry_5a = entity_registry.async_get_or_create( - "light", - "test", - "entity4a-unique", - config_entry=config_entry, - ) - - await create_mock_platform(hass, config_entry, []) - - for entity_entry in ( - entity_entry_1a, - entity_entry_1b, - entity_entry_3a, - entity_entry_4a, - entity_entry_5a, - ): - assert entity_registry.async_get(entity_entry.entity_id) is not None - - entity_entry = entity_registry.async_get(entity_entry_2a.entity_id) - assert entity_entry is not None - # We've removed device so device ID cleared - assert entity_entry.device_id is None - # Removed because only had device tracker entity - assert device_registry.async_get(device_entry_2.id) is None - - @pytest.mark.parametrize( ("mac_address", "unique_id"), [(TEST_MAC_ADDRESS, f"{TEST_MAC_ADDRESS}_yo1")] ) diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 431840d2f57..18f3d64ec0e 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import STATE_HOME, EntityCategory diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 1bbe2394d8e..67c41b85752 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -4,10 +4,9 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation, zone from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_tracker import DOMAIN, device_trigger -import homeassistant.components.zone as zone from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import ( diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 3b95fc9582c..6999a99f7ba 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -5,12 +5,11 @@ import json import logging import os from types import ModuleType -from unittest.mock import Mock, call, patch +from unittest.mock import call, patch import pytest -from homeassistant.components import zone -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components import device_tracker, zone from homeassistant.components.device_tracker import SourceType, const, legacy from homeassistant.const import ( ATTR_ENTITY_PICTURE, @@ -26,11 +25,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery +from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from . import common +from .common import MockScanner, mock_legacy_device_tracker_setup from tests.common import ( assert_setup_component, @@ -58,6 +59,14 @@ def mock_yaml_devices(hass): os.remove(yaml_devices) +@pytest.fixture(autouse=True) +def _mock_legacy_device_tracker_setup( + hass: HomeAssistant, mock_legacy_device_scanner: MockScanner +) -> None: + """Mock legacy device tracker setup.""" + mock_legacy_device_tracker_setup(hass, mock_legacy_device_scanner) + + async def test_is_on(hass: HomeAssistant) -> None: """Test is_on method.""" entity_id = f"{const.DOMAIN}.test" @@ -99,9 +108,7 @@ async def test_reading_broken_yaml_config(hass: HomeAssistant) -> None: assert res[0].dev_id == "my_device" -async def test_reading_yaml_config( - hass: HomeAssistant, yaml_devices, enable_custom_integrations: None -) -> None: +async def test_reading_yaml_config(hass: HomeAssistant, yaml_devices) -> None: """Test the rendering of the YAML configuration.""" dev_id = "test" device = legacy.Device( @@ -179,9 +186,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass: HomeAssistant) -> None: assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file( - hass: HomeAssistant, yaml_devices, enable_custom_integrations: None -) -> None: +async def test_setup_without_yaml_file(hass: HomeAssistant, yaml_devices) -> None: """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -280,13 +285,11 @@ async def test_discover_platform_missing_platform( async def test_update_stale( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, + mock_legacy_device_scanner: MockScanner, ) -> None: """Test stalled update.""" - - scanner = getattr(hass.components, "test.device_tracker").SCANNER - scanner.reset() - scanner.come_home("DEV1") + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("DEV1") now = dt_util.utcnow() register_time = datetime(now.year + 1, 9, 15, 23, tzinfo=dt_util.UTC) @@ -313,7 +316,7 @@ async def test_update_stale( assert hass.states.get("device_tracker.dev1").state == STATE_HOME - scanner.leave_home("DEV1") + mock_legacy_device_scanner.leave_home("DEV1") with patch( "homeassistant.components.device_tracker.legacy.dt_util.utcnow", @@ -328,7 +331,6 @@ async def test_update_stale( async def test_entity_attributes( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test the entity attributes.""" devices = mock_device_tracker_conf @@ -362,9 +364,7 @@ async def test_entity_attributes( @patch("homeassistant.components.device_tracker.legacy.DeviceTracker.async_see") -async def test_see_service( - mock_see, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_see_service(mock_see, hass: HomeAssistant) -> None: """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) @@ -395,13 +395,18 @@ async def test_see_service( async def test_see_service_guard_config_entry( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test the guard if the device is registered in the entity registry.""" - mock_entry = Mock() dev_id = "test" entity_id = f"{const.DOMAIN}.{dev_id}" - mock_registry(hass, {entity_id: mock_entry}) + mock_registry( + hass, + { + entity_id: RegistryEntry( + entity_id=entity_id, unique_id=1, platform=const.DOMAIN + ) + }, + ) devices = mock_device_tracker_conf assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) await hass.async_block_till_done() @@ -416,7 +421,6 @@ async def test_see_service_guard_config_entry( async def test_new_device_event_fired( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test that the device tracker will fire an event.""" with assert_setup_component(1, device_tracker.DOMAIN): @@ -451,7 +455,6 @@ async def test_new_device_event_fired( async def test_duplicate_yaml_keys( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test that the device tracker will not generate invalid YAML.""" devices = mock_device_tracker_conf @@ -471,7 +474,6 @@ async def test_duplicate_yaml_keys( async def test_invalid_dev_id( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test that the device tracker will not allow invalid dev ids.""" devices = mock_device_tracker_conf @@ -485,9 +487,7 @@ async def test_invalid_dev_id( assert not devices -async def test_see_state( - hass: HomeAssistant, yaml_devices, enable_custom_integrations: None -) -> None: +async def test_see_state(hass: HomeAssistant, yaml_devices) -> 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() @@ -527,7 +527,7 @@ async def test_see_state( async def test_see_passive_zone_state( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, + mock_legacy_device_scanner: MockScanner, ) -> None: """Test that the device tracker sets gps for passive trackers.""" now = dt_util.utcnow() @@ -547,9 +547,8 @@ 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") + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("dev1") with ( patch( @@ -581,7 +580,7 @@ async def test_see_passive_zone_state( assert attrs.get("gps_accuracy") == 0 assert attrs.get("source_type") == SourceType.ROUTER - scanner.leave_home("dev1") + mock_legacy_device_scanner.leave_home("dev1") with patch( "homeassistant.components.device_tracker.legacy.dt_util.utcnow", @@ -668,12 +667,11 @@ async def test_bad_platform(hass: HomeAssistant) -> None: async def test_adding_unknown_device_to_config( mock_device_tracker_conf: list[legacy.Device], hass: HomeAssistant, - enable_custom_integrations: None, + mock_legacy_device_scanner: MockScanner, ) -> None: """Test the adding of unknown devices to configuration file.""" - scanner = getattr(hass.components, "test.device_tracker").SCANNER - scanner.reset() - scanner.come_home("DEV1") + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("DEV1") await async_setup_component( hass, device_tracker.DOMAIN, {device_tracker.DOMAIN: {CONF_PLATFORM: "test"}} diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 1aa8e7f829d..48f9bf31f4f 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} await _setup(hass, result) @@ -39,7 +39,7 @@ async def test_form_invalid_credentials_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -62,7 +62,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -72,7 +72,7 @@ async def test_form_advanced_options(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -95,7 +95,7 @@ async def test_form_advanced_options(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "devolo Home Control" assert result2["data"] == { "username": "test-username", @@ -115,7 +115,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await _setup(hass, result) @@ -131,7 +131,7 @@ async def test_form_invalid_credentials_zeroconf(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -150,7 +150,7 @@ async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT result = await hass.config_entries.flow.async_init( DOMAIN, @@ -159,7 +159,7 @@ async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_reauth(hass: HomeAssistant) -> None: @@ -180,7 +180,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -198,7 +198,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 1 @@ -246,7 +246,7 @@ async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -264,7 +264,7 @@ async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "reauth_failed"} @@ -286,7 +286,7 @@ async def _setup(hass: HomeAssistant, result: FlowResult) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "devolo Home Control" assert result2["data"] == { "username": "test-username", diff --git a/tests/components/devolo_home_control/test_diagnostics.py b/tests/components/devolo_home_control/test_diagnostics.py index e31bc360845..f52a9d49017 100644 --- a/tests/components/devolo_home_control/test_diagnostics.py +++ b/tests/components/devolo_home_control/test_diagnostics.py @@ -32,7 +32,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == snapshot diff --git a/tests/components/devolo_home_control/test_siren.py b/tests/components/devolo_home_control/test_siren.py index 037d7b5021f..be662418967 100644 --- a/tests/components/devolo_home_control/test_siren.py +++ b/tests/components/devolo_home_control/test_siren.py @@ -66,7 +66,7 @@ async def test_siren_switching( with patch( "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" - ) as set: + ) as property_set: await hass.services.async_call( "siren", "turn_on", @@ -78,11 +78,11 @@ async def test_siren_switching( "Test", ("devolo.SirenMultiLevelSwitch:Test", 1) ) await hass.async_block_till_done() - set.assert_called_once_with(1) + property_set.assert_called_once_with(1) with patch( "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" - ) as set: + ) as property_set: await hass.services.async_call( "siren", "turn_off", @@ -95,7 +95,7 @@ async def test_siren_switching( ) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF - set.assert_called_once_with(0) + property_set.assert_called_once_with(0) @pytest.mark.usefixtures("mock_zeroconf") @@ -119,7 +119,7 @@ async def test_siren_change_default_tone( with patch( "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" - ) as set: + ) as property_set: test_gateway.publisher.dispatch("Test", ("mss:Test", 2)) await hass.services.async_call( "siren", @@ -127,7 +127,7 @@ async def test_siren_change_default_tone( {"entity_id": f"{DOMAIN}.test"}, blocking=True, ) - set.assert_called_once_with(2) + property_set.assert_called_once_with(2) @pytest.mark.usefixtures("mock_zeroconf") diff --git a/tests/components/devolo_home_network/snapshots/test_button.ambr b/tests/components/devolo_home_network/snapshots/test_button.ambr index 3e8e4ae2bb3..126ac4e7cdb 100644 --- a/tests/components/devolo_home_network/snapshots/test_button.ambr +++ b/tests/components/devolo_home_network/snapshots/test_button.ambr @@ -1,47 +1,4 @@ # serializer version: 1 -# name: test_button[identify_device_with_a_blinking_led-async_identify_device_start] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Identify device with a blinking LED', - 'icon': 'mdi:led-on', - }), - 'context': , - 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[identify_device_with_a_blinking_led-async_identify_device_start].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:led-on', - 'original_name': 'Identify device with a blinking LED', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'identify', - 'unique_id': '1234567890_identify', - 'unit_of_measurement': None, - }) -# --- # name: test_button[identify_device_with_a_blinking_led-plcnet-async_identify_device_start] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -89,49 +46,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_button[restart_device-async_restart] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'restart', - 'friendly_name': 'Mock Title Restart device', - }), - 'context': , - 'entity_id': 'button.mock_title_restart_device', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[restart_device-async_restart].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_title_restart_device', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Restart device', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'restart', - 'unique_id': '1234567890_restart', - 'unit_of_measurement': None, - }) -# --- # name: test_button[restart_device-device-async_restart] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -179,49 +93,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_button[start_plc_pairing-async_pair_device] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Start PLC pairing', - 'icon': 'mdi:plus-network-outline', - }), - 'context': , - 'entity_id': 'button.mock_title_start_plc_pairing', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[start_plc_pairing-async_pair_device].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': None, - 'entity_id': 'button.mock_title_start_plc_pairing', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:plus-network-outline', - 'original_name': 'Start PLC pairing', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'pairing', - 'unique_id': '1234567890_pairing', - 'unit_of_measurement': None, - }) -# --- # name: test_button[start_plc_pairing-plcnet-async_pair_device] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -268,49 +139,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_button[start_wps-async_start_wps] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Start WPS', - 'icon': 'mdi:wifi-plus', - }), - 'context': , - 'entity_id': 'button.mock_title_start_wps', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[start_wps-async_start_wps].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': None, - 'entity_id': 'button.mock_title_start_wps', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:wifi-plus', - 'original_name': 'Start WPS', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'start_wps', - 'unique_id': '1234567890_start_wps', - 'unit_of_measurement': None, - }) -# --- # name: test_button[start_wps-device-async_start_wps] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/devolo_home_network/snapshots/test_image.ambr b/tests/components/devolo_home_network/snapshots/test_image.ambr index ad8ccf43c55..b3924a508cf 100644 --- a/tests/components/devolo_home_network/snapshots/test_image.ambr +++ b/tests/components/devolo_home_network/snapshots/test_image.ambr @@ -11,7 +11,7 @@ 'disabled_by': None, 'domain': 'image', 'entity_category': , - 'entity_id': 'image.mock_title_guest_wifi_credentials_as_qr_code', + 'entity_id': 'image.mock_title_guest_wi_fi_credentials_as_qr_code', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -23,7 +23,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Guest Wifi credentials as QR code', + 'original_name': 'Guest Wi-Fi credentials as QR code', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/devolo_home_network/snapshots/test_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_sensor.ambr index fc173da8294..d985ac35495 100644 --- a/tests/components/devolo_home_network/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_network/snapshots/test_sensor.ambr @@ -45,21 +45,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[connected_wifi_clients-async_get_wifi_connected_station-interval0] +# name: test_sensor[connected_wi_fi_clients-async_get_wifi_connected_station-interval0] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Connected Wifi clients', + 'friendly_name': 'Mock Title Connected Wi-Fi clients', 'state_class': , }), 'context': , - 'entity_id': 'sensor.mock_title_connected_wifi_clients', + 'entity_id': 'sensor.mock_title_connected_wi_fi_clients', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1', }) # --- -# name: test_sensor[connected_wifi_clients-async_get_wifi_connected_station-interval0].1 +# name: test_sensor[connected_wi_fi_clients-async_get_wifi_connected_station-interval0].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -73,7 +73,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.mock_title_connected_wifi_clients', + 'entity_id': 'sensor.mock_title_connected_wi_fi_clients', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -85,7 +85,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Connected Wifi clients', + 'original_name': 'Connected Wi-Fi clients', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, @@ -94,20 +94,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[neighboring_wifi_networks-async_get_wifi_neighbor_access_points-interval1] +# name: test_sensor[neighboring_wi_fi_networks-async_get_wifi_neighbor_access_points-interval1] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Neighboring Wifi networks', + 'friendly_name': 'Mock Title Neighboring Wi-Fi networks', }), 'context': , - 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', + 'entity_id': 'sensor.mock_title_neighboring_wi_fi_networks', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1', }) # --- -# name: test_sensor[neighboring_wifi_networks-async_get_wifi_neighbor_access_points-interval1].1 +# name: test_sensor[neighboring_wi_fi_networks-async_get_wifi_neighbor_access_points-interval1].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -119,7 +119,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', + 'entity_id': 'sensor.mock_title_neighboring_wi_fi_networks', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -131,7 +131,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Neighboring Wifi networks', + 'original_name': 'Neighboring Wi-Fi networks', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/devolo_home_network/snapshots/test_switch.ambr b/tests/components/devolo_home_network/snapshots/test_switch.ambr index 09b56efc784..a2df5d2579f 100644 --- a/tests/components/devolo_home_network/snapshots/test_switch.ambr +++ b/tests/components/devolo_home_network/snapshots/test_switch.ambr @@ -1,97 +1,11 @@ # serializer version: 1 -# name: test_switches[enable_guest_wifi-async_get_wifi_guest_access-async_set_wifi_guest_access-interval0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Enable guest Wifi', - 'icon': 'mdi:wifi', - }), - 'context': , - 'entity_id': 'switch.mock_title_enable_guest_wifi', - 'last_changed': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switches[enable_guest_wifi-async_get_wifi_guest_access-async_set_wifi_guest_access-interval0].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.mock_title_enable_guest_wifi', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Enable guest Wifi', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1234567890_switch_guest_wifi', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[enable_leds-async_get_led_setting-async_set_led_setting-interval1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Enable LEDs', - 'icon': 'mdi:led-off', - }), - 'context': , - 'entity_id': 'switch.mock_title_enable_leds', - 'last_changed': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switches[enable_leds-async_get_led_setting-async_set_led_setting-interval1].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.mock_title_enable_leds', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:led-off', - 'original_name': 'Enable LEDs', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1234567890_switch_leds', - 'unit_of_measurement': None, - }) -# --- # name: test_update_enable_guest_wifi StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Enable guest Wifi', + 'friendly_name': 'Mock Title Enable guest Wi-Fi', }), 'context': , - 'entity_id': 'switch.mock_title_enable_guest_wifi', + 'entity_id': 'switch.mock_title_enable_guest_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , @@ -110,7 +24,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.mock_title_enable_guest_wifi', + 'entity_id': 'switch.mock_title_enable_guest_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -122,7 +36,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Enable guest Wifi', + 'original_name': 'Enable guest Wi-Fi', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 5d23037df54..5aa2bfa274e 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["result"].unique_id == info["serial_number"] assert result2["title"] == info["title"] assert result2["data"] == { @@ -83,7 +83,7 @@ async def test_form_error(hass: HomeAssistant, exception_type, expected_error) - }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_BASE: expected_error} @@ -96,7 +96,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {"host_name": "test"} context = next( @@ -134,7 +134,7 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_WRONG_DEVICE, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "home_control" @@ -158,7 +158,7 @@ async def test_abort_if_configued(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Abort on concurrent zeroconf discovery flow @@ -167,7 +167,7 @@ async def test_abort_if_configued(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_CHANGED, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == IP_ALT @@ -192,7 +192,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.devolo_home_network.async_setup_entry", @@ -204,7 +204,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/devolo_home_network/test_diagnostics.py b/tests/components/devolo_home_network/test_diagnostics.py index 75794250908..a3580cac954 100644 --- a/tests/components/devolo_home_network/test_diagnostics.py +++ b/tests/components/devolo_home_network/test_diagnostics.py @@ -25,7 +25,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == snapshot diff --git a/tests/components/devolo_home_network/test_image.py b/tests/components/devolo_home_network/test_image.py index 0ca3936e1ac..80efc4fcc09 100644 --- a/tests/components/devolo_home_network/test_image.py +++ b/tests/components/devolo_home_network/test_image.py @@ -32,7 +32,7 @@ async def test_image_setup(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert ( - hass.states.get(f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code") + hass.states.get(f"{DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code") is not None ) @@ -51,13 +51,13 @@ async def test_guest_wifi_qr( """Test showing a QR code of the guest wifi credentials.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code" + state_key = f"{DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(state_key) - assert state.name == "Mock Title Guest Wifi credentials as QR code" + assert state.name == "Mock Title Guest Wi-Fi credentials as QR code" assert state.state == dt_util.utcnow().isoformat() assert entity_registry.async_get(state_key) == snapshot diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 5b5e05a40d1..efcbaa803df 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -32,9 +32,11 @@ async def test_sensor_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.{device_name}_connected_wifi_clients") is not None + assert ( + hass.states.get(f"{DOMAIN}.{device_name}_connected_wi_fi_clients") is not None + ) assert hass.states.get(f"{DOMAIN}.{device_name}_connected_plc_devices") is None - assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wifi_networks") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wi_fi_networks") is None assert ( hass.states.get( f"{DOMAIN}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[1].user_device_name}" @@ -67,12 +69,12 @@ async def test_sensor_setup(hass: HomeAssistant) -> None: ("name", "get_method", "interval"), [ ( - "connected_wifi_clients", + "connected_wi_fi_clients", "async_get_wifi_connected_station", SHORT_UPDATE_INTERVAL, ), ( - "neighboring_wifi_networks", + "neighboring_wi_fi_networks", "async_get_wifi_neighbor_access_points", LONG_UPDATE_INTERVAL, ), diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index 0fe5bea5c52..b96697dc9cc 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -41,7 +41,7 @@ async def test_switch_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wifi") is not None + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wi_fi") is not None assert hass.states.get(f"{PLATFORM}.{device_name}_enable_leds") is not None await hass.config_entries.async_unload(entry.entry_id) @@ -82,7 +82,7 @@ async def test_update_enable_guest_wifi( """Test state change of a enable_guest_wifi switch device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_enable_guest_wifi" + state_key = f"{PLATFORM}.{device_name}_enable_guest_wi_fi" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -247,7 +247,7 @@ 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_guest_wi_fi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL), ("enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL), ], ) @@ -284,7 +284,7 @@ async def test_device_failure( @pytest.mark.parametrize( ("name", "set_method"), [ - ("enable_guest_wifi", "async_set_wifi_guest_access"), + ("enable_guest_wi_fi", "async_set_wifi_guest_access"), ("enable_leds", "async_set_led_setting"), ], ) diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index f87f365a7e6..e8893e21d0e 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pydexcom import AccountError, SessionError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONFIG @@ -20,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_USERNAME] assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +61,7 @@ async def test_form_account_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -79,7 +80,7 @@ async def test_form_session_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -98,7 +99,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -113,14 +114,14 @@ async def test_option_flow_default(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_UNIT_OF_MEASUREMENT: MG_DL, } @@ -137,14 +138,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UNIT_OF_MEASUREMENT: MMOL_L, } diff --git a/tests/components/diagnostics/__init__.py b/tests/components/diagnostics/__init__.py index 81d62e7c2fe..d241ca09f41 100644 --- a/tests/components/diagnostics/__init__.py +++ b/tests/components/diagnostics/__init__.py @@ -19,6 +19,7 @@ async def _get_diagnostics_for_config_entry( ) -> JsonObjectType: """Return the diagnostics config entry for the specified domain.""" assert await async_setup_component(hass, "diagnostics", {}) + await hass.async_block_till_done() client = await hass_client() response = await client.get( diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index 3303e51a5a5..dff71d9edbf 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -80,7 +80,9 @@ async def test_websocket( async def test_download_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + enable_custom_integrations: None, ) -> None: """Test download diagnostics.""" config_entry = MockConfigEntry(domain="fake_integration") @@ -91,7 +93,73 @@ async def test_download_diagnostics( assert await _get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "home_assistant": hass_sys_info, - "custom_components": {}, + "custom_components": { + "test": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_blocked_version": { + "documentation": None, + "requirements": [], + "version": "1.0.0", + }, + "test_embedded": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_frame": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_platform": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations_bad_data": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_executor": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_loop": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error_config_entry": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_with_services": { + "documentation": None, + "requirements": [], + "version": "1.0", + }, + }, "integration_manifest": { "codeowners": [], "dependencies": [], @@ -112,7 +180,73 @@ async def test_download_diagnostics( hass, hass_client, config_entry, device ) == { "home_assistant": hass_sys_info, - "custom_components": {}, + "custom_components": { + "test": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_blocked_version": { + "documentation": None, + "requirements": [], + "version": "1.0.0", + }, + "test_embedded": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_frame": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_platform": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations_bad_data": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_executor": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_loop": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error_config_entry": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_with_services": { + "documentation": None, + "requirements": [], + "version": "1.0", + }, + }, "integration_manifest": { "codeowners": [], "dependencies": [], diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 7f5fe79d146..a977a414fe4 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -6,10 +6,11 @@ import json import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dialogflow, intent_script from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d" @@ -88,10 +89,10 @@ async def fixture(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "dialogflow", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] return await hass_client_no_auth(), webhook_id diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 569e165a0a6..ad22aa871b7 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -33,7 +33,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_ssdp_form( @@ -47,7 +47,7 @@ async def test_show_ssdp_form( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -65,7 +65,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +83,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -100,7 +100,7 @@ async def test_ssdp_confirm_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -117,7 +117,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -134,7 +134,7 @@ async def test_ssdp_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_ssdp_with_receiver_id_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -171,7 +171,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -190,7 +190,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -209,7 +209,7 @@ async def test_ssdp_confirm_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -224,7 +224,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -234,7 +234,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -253,7 +253,7 @@ async def test_full_ssdp_flow_implementation( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -261,7 +261,7 @@ async def test_full_ssdp_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] diff --git a/tests/components/discord/conftest.py b/tests/components/discord/conftest.py index 128869d0b80..c9c4e7c17d5 100644 --- a/tests/components/discord/conftest.py +++ b/tests/components/discord/conftest.py @@ -29,7 +29,7 @@ def discord_aiohttp_mock_factory( """Create Discord service mock from factory.""" def _discord_aiohttp_mock_factory( - headers: dict[str, str] = None, + headers: dict[str, str] | None = None, ) -> AiohttpClientMocker: if headers is not None: aioclient_mock.get( diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index ba1909c48c8..9b37179e86d 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -2,10 +2,11 @@ import nextcord -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.discord.const import DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -29,7 +30,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -46,7 +47,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -59,7 +60,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -68,7 +69,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -82,7 +83,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -91,7 +92,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -105,7 +106,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -114,7 +115,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -132,7 +133,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_conf = {CONF_API_TOKEN: "1234567890123"} @@ -142,7 +143,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -151,6 +152,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | new_conf diff --git a/tests/components/discovergy/test_config_flow.py b/tests/components/discovergy/test_config_flow.py index b8da429d881..2464ba3846f 100644 --- a/tests/components/discovergy/test_config_flow.py +++ b/tests/components/discovergy/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch from pydiscovergy.error import DiscovergyClientError, HTTPError, InvalidLogin import pytest -from homeassistant import data_entry_flow from homeassistant.components.discovergy.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER 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 @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant, discovergy: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant, discovergy: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test@example.com" assert result2["data"] == { CONF_EMAIL: "test@example.com", @@ -56,7 +56,7 @@ async def test_reauth( data=None, ) - assert init_result["type"] == data_entry_flow.FlowResultType.FORM + assert init_result["type"] is FlowResultType.FORM assert init_result["step_id"] == "reauth" with patch( @@ -72,7 +72,7 @@ async def test_reauth( ) await hass.async_block_till_done() - assert configure_result["type"] == data_entry_flow.FlowResultType.ABORT + assert configure_result["type"] is FlowResultType.ABORT assert configure_result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -100,7 +100,7 @@ async def test_form_fail( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": message} @@ -114,6 +114,6 @@ async def test_form_fail( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test@example.com" assert "errors" not in result diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py index 01e61f7a8fa..b6f025bb5b0 100644 --- a/tests/components/dlink/test_config_flow.py +++ b/tests/components/dlink/test_config_flow.py @@ -2,12 +2,12 @@ from unittest.mock import MagicMock, patch -from homeassistant import data_entry_flow from homeassistant.components import dhcp from homeassistant.components.dlink.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( CONF_DATA, @@ -35,7 +35,7 @@ async def test_flow_user(hass: HomeAssistant, mocked_plug: MagicMock) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -48,7 +48,7 @@ async def test_flow_user_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -62,7 +62,7 @@ async def test_flow_user_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -71,7 +71,7 @@ async def test_flow_user_cannot_connect( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -85,7 +85,7 @@ async def test_flow_user_unknown_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -94,7 +94,7 @@ async def test_flow_user_unknown_error( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -104,14 +104,14 @@ async def test_dhcp(hass: HomeAssistant, mocked_plug: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" with patch_config_flow(mocked_plug), _patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -123,14 +123,14 @@ async def test_dhcp_failed_legacy_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" with patch_config_flow(mocked_plug_legacy_no_auth): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" with patch_config_flow(mocked_plug), _patch_setup_entry(): @@ -138,7 +138,7 @@ async def test_dhcp_failed_legacy_auth( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -151,7 +151,7 @@ async def test_dhcp_already_configured( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "aabbccddeeff" @@ -168,14 +168,14 @@ async def test_dhcp_unique_id_assignment( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" with patch_config_flow(mocked_plug), _patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA | {CONF_HOST: "2.3.4.5"} assert result["result"].unique_id == "11:22:33:44:55:66" @@ -188,6 +188,6 @@ async def test_dhcp_changed_ip( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW_NEW_IP ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_with_uid.data[CONF_HOST] == "5.6.7.8" diff --git a/tests/components/dlink/test_init.py b/tests/components/dlink/test_init.py index 484927340fa..43055b681e0 100644 --- a/tests/components/dlink/test_init.py +++ b/tests/components/dlink/test_init.py @@ -19,7 +19,7 @@ async def test_setup_config_and_unload( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -37,7 +37,7 @@ async def test_legacy_setup_config_and_unload( await setup_integration_legacy() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -56,7 +56,7 @@ async def test_async_setup_entry_not_ready( """Test that it throws ConfigEntryNotReady when exception occurs during legacy setup.""" with patch_setup(mocked_plug_legacy_no_auth): await hass.config_entries.async_setup(config_entry_with_uid.entry_id) - assert config_entry_with_uid.state == ConfigEntryState.SETUP_RETRY + assert config_entry_with_uid.state is ConfigEntryState.SETUP_RETRY async def test_device_info( diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index bb47a468dc4..59b1af546f2 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -82,7 +82,7 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: @pytest.fixture def config_entry_mock() -> MockConfigEntry: """Mock a config entry for this platform.""" - mock_entry = MockConfigEntry( + return MockConfigEntry( unique_id=MOCK_DEVICE_UDN, domain=DLNA_DOMAIN, data={ @@ -94,13 +94,12 @@ def config_entry_mock() -> MockConfigEntry: title=MOCK_DEVICE_NAME, options={}, ) - return mock_entry @pytest.fixture def config_entry_mock_no_mac() -> MockConfigEntry: """Mock a config entry that does not already contain a MAC address.""" - mock_entry = MockConfigEntry( + return MockConfigEntry( unique_id=MOCK_DEVICE_UDN, domain=DLNA_DOMAIN, data={ @@ -111,7 +110,6 @@ def config_entry_mock_no_mac() -> MockConfigEntry: title=MOCK_DEVICE_NAME, options={}, ) - return mock_entry @pytest.fixture diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 32cfd8ad5a9..55cf20859d3 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -112,7 +112,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -120,7 +120,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -144,7 +144,7 @@ async def test_user_flow_discovered_manual( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -152,7 +152,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -160,7 +160,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -182,7 +182,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -190,7 +190,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -211,7 +211,7 @@ async def test_user_flow_uncontactable( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -219,7 +219,7 @@ async def test_user_flow_uncontactable( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "manual" @@ -244,7 +244,7 @@ async def test_user_flow_embedded_st( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -252,7 +252,7 @@ async def test_user_flow_embedded_st( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -272,7 +272,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -280,7 +280,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "not_dmr"} assert result["step_id"] == "manual" @@ -295,7 +295,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -303,7 +303,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -327,7 +327,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -337,7 +337,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -368,7 +368,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -388,7 +388,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -414,7 +414,7 @@ async def test_ssdp_duplicate_mac_ignored_entry( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -437,7 +437,7 @@ async def test_ssdp_duplicate_mac_configured_entry( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -453,7 +453,7 @@ async def test_ssdp_add_mac( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -474,7 +474,7 @@ async def test_ssdp_dont_remove_mac( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -502,7 +502,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -518,7 +518,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" # Service list does not contain services @@ -530,7 +530,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" # AVTransport service is missing @@ -546,7 +546,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -568,7 +568,7 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -582,7 +582,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) @@ -595,7 +595,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" for manufacturer, model in [ @@ -613,7 +613,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" @@ -635,7 +635,7 @@ async def test_ignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -659,7 +659,7 @@ async def test_ignore_flow_no_ssdp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: None, @@ -680,7 +680,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME # Device was found via SSDP, matching the 2nd device type tried @@ -698,7 +698,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -706,7 +706,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -730,7 +730,7 @@ async def test_unignore_flow_offline( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME # Device is not in the SSDP discoveries (perhaps HA restarted between ignore and unignore) @@ -742,7 +742,7 @@ async def test_unignore_flow_offline( context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_error" @@ -756,7 +756,7 @@ async def test_get_mac_address_ipv4( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" mock_get_mac_address.assert_called_once_with(ip=MOCK_DEVICE_HOST_ADDR) @@ -780,7 +780,7 @@ async def test_get_mac_address_ipv6( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # The scope must be removed for get_mac_address to work correctly @@ -821,7 +821,7 @@ async def test_options_flow( config_entry_mock.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry_mock.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -835,7 +835,7 @@ async def test_options_flow( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_url"} @@ -850,7 +850,7 @@ async def test_options_flow( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 9ead49f0955..87c54c2956b 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -85,9 +85,7 @@ async def setup_mock_component(hass: HomeAssistant, mock_entry: MockConfigEntry) entries = async_entries_for_config_entry(async_get_er(hass), mock_entry.entry_id) assert len(entries) == 1 - entity_id = entries[0].entity_id - - return entity_id + return entries[0].entity_id async def get_attrs(hass: HomeAssistant, entity_id: str) -> Mapping[str, Any]: diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index eacf03e9da7..c1bee224c5a 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -93,7 +93,7 @@ def aiohttp_session_requester_mock() -> Iterable[Mock]: @pytest.fixture def config_entry_mock() -> MockConfigEntry: """Mock a config entry for this platform.""" - mock_entry = MockConfigEntry( + return MockConfigEntry( unique_id=MOCK_DEVICE_USN, domain=DOMAIN, version=CONFIG_VERSION, @@ -104,7 +104,6 @@ def config_entry_mock() -> MockConfigEntry: }, title=MOCK_DEVICE_NAME, ) - return mock_entry @pytest.fixture diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index 8a2bda611a7..b61b4a42c49 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -11,11 +11,12 @@ from unittest.mock import Mock, patch from async_upnp_client.exceptions import UpnpError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.dlna_dms.const import CONF_SOURCE_ID, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_DEVICE_HOST, @@ -88,7 +89,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -97,7 +98,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -121,7 +122,7 @@ async def test_user_flow_no_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -135,7 +136,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -143,7 +144,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -166,7 +167,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" upnp_factory_mock.async_create_device.side_effect = UpnpError @@ -176,7 +177,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -206,7 +207,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -221,7 +222,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -235,7 +236,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bad_ssdp" # Missing USN @@ -245,7 +246,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bad_ssdp" @@ -285,7 +286,7 @@ async def test_duplicate_name( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -293,7 +294,7 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: new_device_location, @@ -323,7 +324,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -339,7 +340,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" # Service list does not contain services @@ -351,7 +352,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" # ContentDirectory service is missing @@ -367,7 +368,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" @@ -389,5 +390,5 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 47bd7b0b39b..bb3c9230534 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Final, Union +from typing import Final from unittest.mock import ANY, Mock, call from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError @@ -38,7 +38,7 @@ pytestmark = [ ] -BrowseResultList = list[Union[didl_lite.DidlObject, didl_lite.Descriptor]] +BrowseResultList = list[didl_lite.DidlObject | didl_lite.Descriptor] async def async_resolve_media( @@ -249,7 +249,7 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> res_mime: Final = "audio/mpeg" search_directory_result = [] - for ob_id, ob_title in zip(object_ids, path.split("/")): + for ob_id, ob_title in zip(object_ids, path.split("/"), strict=False): didl_item = didl_lite.Item( id=ob_id, restricted="false", @@ -274,7 +274,9 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + for parent_id, title in zip( + ["0"] + object_ids[:-1], path.split("/"), strict=False + ) ] assert result.url == res_abs_url assert result.mime_type == res_mime @@ -290,7 +292,9 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + for parent_id, title in zip( + ["0"] + object_ids[:-1], path.split("/"), strict=False + ) ] assert result.url == res_abs_url assert result.mime_type == res_mime @@ -305,7 +309,7 @@ async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) # Setup expected calls search_directory_result = [] - for ob_id, ob_title in zip(object_ids, path.split("/")): + for ob_id, ob_title in zip(object_ids, path.split("/"), strict=False): didl_item = didl_lite.Item( id=ob_id, restricted="false", @@ -346,7 +350,9 @@ async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + for parent_id, title in zip( + ["0"] + object_ids[:-1], path.split("/"), strict=False + ) ] assert result.didl_metadata.id == object_ids[-1] # 2nd level should also be browsed @@ -608,7 +614,7 @@ async def test_browse_media_object(hass: HomeAssistant, dms_device_mock: Mock) - assert not result.can_play assert result.can_expand assert result.children - for child, title in zip(result.children, child_titles): + for child, title in zip(result.children, child_titles, strict=False): assert isinstance(child, BrowseMediaSource) assert child.identifier == f"{MOCK_SOURCE_ID}/:{title}_id" assert child.title == title @@ -746,7 +752,7 @@ async def test_browse_media_search(hass: HomeAssistant, dms_device_mock: Mock) - assert result.title == "Search results" assert result.children - for obj, child in zip(object_details, result.children): + for obj, child in zip(object_details, result.children, strict=False): assert isinstance(child, BrowseMediaSource) assert child.identifier == f"{MOCK_SOURCE_ID}/:{obj[0]}" assert child.title == obj[1] diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 5bfa1539d44..ff089be0e1e 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.dnsip.const import ( CONF_RESOLVER_IPV6, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["data_schema"] == DATA_SCHEMA assert result["errors"] == {} @@ -54,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -99,7 +100,7 @@ async def test_form_adv(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -132,7 +133,7 @@ async def test_form_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_hostname"} @@ -178,7 +179,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -209,7 +210,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -221,13 +222,13 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2001:4860:4860::8888", } - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_options_flow_empty_return(hass: HomeAssistant) -> None: @@ -257,7 +258,7 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -266,7 +267,7 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "208.67.222.222", "resolver_ipv6": "2620:119:53::53", @@ -337,7 +338,7 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "init" if p_input[CONF_IPV4]: assert result2["errors"] == {"resolver": "invalid_resolver"} diff --git a/tests/components/dnsip/test_init.py b/tests/components/dnsip/test_init.py index 37595444c44..3d816bebe60 100644 --- a/tests/components/dnsip/test_init.py +++ b/tests/components/dnsip/test_init.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components.dnsip.const import ( CONF_HOSTNAME, CONF_IPV4, @@ -13,7 +12,7 @@ from homeassistant.components.dnsip.const import ( CONF_RESOLVER_IPV6, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -49,7 +48,7 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 4939bada6f8..cd4ddccda87 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -6,11 +6,12 @@ from unittest.mock import MagicMock, Mock, patch import pytest import requests -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -46,7 +47,7 @@ async def test_user_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} doorbirdapi = _get_mock_doorbirdapi_return_values( @@ -71,7 +72,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.2.3.4" assert result2["data"] == { "host": "1.2.3.4", @@ -99,7 +100,7 @@ async def test_form_zeroconf_wrong_oui(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -119,7 +120,7 @@ async def test_form_zeroconf_link_local_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -146,7 +147,7 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "4.4.4.4" @@ -167,7 +168,7 @@ async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -195,7 +196,7 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -218,7 +219,7 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.2.3.4" assert result2["data"] == { "host": "1.2.3.4", @@ -265,7 +266,7 @@ async def test_form_zeroconf_correct_oui_wrong_device( ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -285,7 +286,7 @@ async def test_form_user_cannot_connect(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -306,7 +307,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -326,12 +327,12 @@ async def test_options_flow(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EVENTS: "eventa, eventc, eventq"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]} diff --git a/tests/components/dormakaba_dkey/test_config_flow.py b/tests/components/dormakaba_dkey/test_config_flow.py index d29e176bb7e..499e5844949 100644 --- a/tests/components/dormakaba_dkey/test_config_flow.py +++ b/tests/components/dormakaba_dkey/test_config_flow.py @@ -27,7 +27,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,7 +37,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -53,7 +53,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -74,7 +74,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -88,7 +88,7 @@ async def test_user_step_device_added_between_steps_1(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -101,7 +101,7 @@ async def test_user_step_device_added_between_steps_1(hass: HomeAssistant) -> No result["flow_id"], user_input={"address": DKEY_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -114,7 +114,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -125,7 +125,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -133,7 +133,7 @@ async def test_async_step_user_takes_precedence_over_discovery( CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -150,12 +150,12 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -178,7 +178,7 @@ async def _test_common_success(hass: HomeAssistant, result: FlowResult) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DKEY_DISCOVERY_INFO.name assert result["data"] == { CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, @@ -200,7 +200,7 @@ async def test_bluetooth_step_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -211,7 +211,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -219,7 +219,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -237,17 +237,17 @@ async def test_bluetooth_step_cannot_connect(hass: HomeAssistant, exc, error) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -258,7 +258,7 @@ async def test_bluetooth_step_cannot_connect(hass: HomeAssistant, exc, error) -> result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -276,17 +276,17 @@ async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error) context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -297,7 +297,7 @@ async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] == {"base": error} @@ -315,7 +315,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -328,7 +328,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "no_longer_in_range"} @@ -342,7 +342,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -359,7 +359,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py index 897fbba0c59..132b83dffdf 100644 --- a/tests/components/downloader/test_config_flow.py +++ b/tests/components/downloader/test_config_flow.py @@ -5,7 +5,6 @@ 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 @@ -21,40 +20,36 @@ async def test_user_form(hass: HomeAssistant) -> None: 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=CONFIG, + ) + assert result["type"] is FlowResultType.FORM - with patch( - "homeassistant.components.downloader.async_setup_entry", - return_value=True, - ): + with patch("os.path.isdir", return_value=False): 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"} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "directory_does_not_exist"} with ( patch( "homeassistant.components.downloader.async_setup_entry", return_value=True ), patch( - "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", - return_value=None, + "os.path.isdir", + return_value=True, ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG, ) - - assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Downloader" assert result["data"] == {"download_dir": "download_dir"} @@ -66,14 +61,13 @@ async def test_single_instance_allowed( ) -> 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["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -84,21 +78,19 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: "homeassistant.components.downloader.async_setup_entry", return_value=True ), patch( - "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", - return_value=None, + "os.path.isdir", + return_value=True, ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={}, + data=CONFIG, ) await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Downloader" - assert result["data"] == {} - assert result["options"] == {} + assert result["data"] == CONFIG async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: @@ -112,6 +104,5 @@ async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "directory_does_not_exist" diff --git a/tests/components/dremel_3d_printer/test_config_flow.py b/tests/components/dremel_3d_printer/test_config_flow.py index 938068aa9b0..5484f1e1191 100644 --- a/tests/components/dremel_3d_printer/test_config_flow.py +++ b/tests/components/dremel_3d_printer/test_config_flow.py @@ -23,7 +23,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry(): @@ -31,7 +31,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "DREMEL 3D45" assert result["data"] == CONF_DATA @@ -43,7 +43,7 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -54,7 +54,7 @@ async def test_cannot_connect(hass: HomeAssistant, connection) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -63,7 +63,7 @@ async def test_cannot_connect(hass: HomeAssistant, connection) -> None: result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA @@ -74,7 +74,7 @@ async def test_unknown_error(hass: HomeAssistant, connection) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -83,6 +83,6 @@ async def test_unknown_error(hass: HomeAssistant, connection) -> None: result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "DREMEL 3D45" assert result["data"] == CONF_DATA diff --git a/tests/components/dremel_3d_printer/test_init.py b/tests/components/dremel_3d_printer/test_init.py index 8216054587d..6b008c7fac1 100644 --- a/tests/components/dremel_3d_printer/test_init.py +++ b/tests/components/dremel_3d_printer/test_init.py @@ -27,7 +27,7 @@ async def test_setup( with patch(MOCKED_MODEL, return_value=model) as mock: await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert mock.called with patch(MOCKED_MODEL, return_value=model) as mock: @@ -50,7 +50,7 @@ async def test_async_setup_entry_not_ready( await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -60,7 +60,7 @@ async def test_update_failed( """Test coordinator throws UpdateFailed after failed update.""" await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED with patch( "homeassistant.components.dremel_3d_printer.Dremel3DPrinter.refresh", diff --git a/tests/components/drop_connect/test_config_flow.py b/tests/components/drop_connect/test_config_flow.py index 180b6fef860..8cd765b46b4 100644 --- a/tests/components/drop_connect/test_config_flow.py +++ b/tests/components/drop_connect/test_config_flow.py @@ -24,7 +24,7 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N data=discovery_info, ) assert result is not None - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -32,7 +32,7 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N ) await hass.async_block_till_done() assert result is not None - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "drop_command_topic": "drop_connect/DROP-1_C0FFEE/cmd/255", "drop_data_topic": "drop_connect/DROP-1_C0FFEE/data/255/#", @@ -61,7 +61,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No data=discovery_info, ) assert result is not None - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -69,7 +69,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No ) await hass.async_block_till_done() assert result is not None - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Attempting configuration of the same object should abort result = await hass.config_entries.flow.async_init( @@ -78,7 +78,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -100,7 +100,7 @@ async def test_mqtt_setup_incomplete_payload( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -122,7 +122,7 @@ async def test_mqtt_setup_bad_json( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -144,7 +144,7 @@ async def test_mqtt_setup_bad_topic( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -166,7 +166,7 @@ async def test_mqtt_setup_no_payload( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -175,5 +175,5 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "drop_connect", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 687c6b4a3bc..791797f7dcd 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -9,7 +9,7 @@ import pytest import serial import serial.tools.list_ports -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.dsmr import DOMAIN, config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -39,7 +39,7 @@ async def test_setup_network( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -48,7 +48,7 @@ async def test_setup_network( {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -70,7 +70,7 @@ async def test_setup_network( "protocol": "dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.0.1:1234" assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -87,7 +87,7 @@ async def test_setup_network_rfxtrx( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -96,7 +96,7 @@ async def test_setup_network_rfxtrx( {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -121,7 +121,7 @@ async def test_setup_network_rfxtrx( "protocol": "rfxtrx_dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.0.1:1234" assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -196,7 +196,7 @@ async def test_setup_serial( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -205,7 +205,7 @@ async def test_setup_serial( {"type": "Serial"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -216,7 +216,7 @@ async def test_setup_serial( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == port.device assert result["data"] == entry_data @@ -237,7 +237,7 @@ async def test_setup_serial_rfxtrx( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -246,7 +246,7 @@ async def test_setup_serial_rfxtrx( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -266,7 +266,7 @@ async def test_setup_serial_rfxtrx( "protocol": "rfxtrx_dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == port.device assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -280,7 +280,7 @@ async def test_setup_serial_manual( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -289,7 +289,7 @@ async def test_setup_serial_manual( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -298,7 +298,7 @@ async def test_setup_serial_manual( {"port": "Enter Manually", "dsmr_version": "2.2"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] is None @@ -314,7 +314,7 @@ async def test_setup_serial_manual( "protocol": "dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyUSB0" assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -338,7 +338,7 @@ async def test_setup_serial_fail( side_effect=chain([serial.SerialException], repeat(DEFAULT)), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -347,7 +347,7 @@ async def test_setup_serial_fail( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -360,7 +360,7 @@ async def test_setup_serial_fail( {"port": port.device, "dsmr_version": "2.2"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_connect"} @@ -398,7 +398,7 @@ async def test_setup_serial_timeout( ) rfxtrx_protocol.wait_closed = first_timeout_wait_closed - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -407,7 +407,7 @@ async def test_setup_serial_timeout( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -416,7 +416,7 @@ async def test_setup_serial_timeout( result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_communicate"} @@ -442,7 +442,7 @@ async def test_setup_serial_wrong_telegram( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -451,7 +451,7 @@ async def test_setup_serial_wrong_telegram( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -463,7 +463,7 @@ async def test_setup_serial_wrong_telegram( {"port": port.device, "dsmr_version": "2.2"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_communicate"} @@ -485,7 +485,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -499,7 +499,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: 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 + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 95def2f66cf..429128c48bb 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -6,13 +6,16 @@ from decimal import Decimal from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry async def test_migrate_gas_to_mbus( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture, ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -42,7 +45,6 @@ async def test_migrate_gas_to_mbus( old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" - device_registry = hass.helpers.device_registry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, identifiers={(DOMAIN, mock_entry.entry_id)}, @@ -108,7 +110,10 @@ async def test_migrate_gas_to_mbus( async def test_migrate_gas_to_mbus_exists( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture, ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -138,7 +143,6 @@ async def test_migrate_gas_to_mbus_exists( old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" - device_registry = hass.helpers.device_registry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, identifiers={(DOMAIN, mock_entry.entry_id)}, diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index f63422e0543..7a38e3010d8 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -13,13 +13,13 @@ from unittest.mock import DEFAULT, MagicMock import pytest -from homeassistant import config_entries from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -1325,7 +1325,7 @@ async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED async def test_gas_meter_providing_energy_reading( diff --git a/tests/components/dsmr_reader/snapshots/test_diagnostics.ambr b/tests/components/dsmr_reader/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c6bc616ffd3 --- /dev/null +++ b/tests/components/dsmr_reader/snapshots/test_diagnostics.ambr @@ -0,0 +1,23 @@ +# serializer version: 1 +# name: test_get_config_entry_diagnostics + dict({ + 'entities': dict({ + }), + 'entry': dict({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'dsmr_reader', + 'entry_id': 'TEST_ENTRY_ID', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'dsmr_reader', + 'unique_id': 'UNIQUE_TEST_ID', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/dsmr_reader/test_config_flow.py b/tests/components/dsmr_reader/test_config_flow.py index cc605eaa49c..e31e4f154c0 100644 --- a/tests/components/dsmr_reader/test_config_flow.py +++ b/tests/components/dsmr_reader/test_config_flow.py @@ -12,7 +12,7 @@ async def test_user_step(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -20,12 +20,12 @@ async def test_user_step(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == "DSMR Reader" duplicate_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert duplicate_result["type"] == FlowResultType.ABORT + assert duplicate_result["type"] is FlowResultType.ABORT assert duplicate_result["reason"] == "single_instance_allowed" diff --git a/tests/components/dsmr_reader/test_diagnostics.py b/tests/components/dsmr_reader/test_diagnostics.py new file mode 100644 index 00000000000..553efd0b38b --- /dev/null +++ b/tests/components/dsmr_reader/test_diagnostics.py @@ -0,0 +1,39 @@ +"""Test the DSMR Reader component diagnostics.""" + +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.dsmr_reader.const import DOMAIN +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, + snapshot: SnapshotAssertion, + hass_client: ClientSessionGenerator, +) -> None: + """Test if get_config_entry_diagnostics returns the correct data.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DOMAIN, + options={}, + entry_id="TEST_ENTRY_ID", + unique_id="UNIQUE_TEST_ID", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dsmr_reader.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + assert diagnostics == snapshot diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index bf3137e0204..a35c1eec4cc 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -2,11 +2,11 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.dunehd.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -77,7 +77,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "dunehd-host" assert result["data"] == {CONF_HOST: "dunehd-host"} @@ -94,6 +94,6 @@ async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "2001:db8::1428:57ab" assert result["data"] == {CONF_HOST: "2001:db8::1428:57ab"} diff --git a/tests/components/duotecno/test_config_flow.py b/tests/components/duotecno/test_config_flow.py index b62b6e90801..77946babd8c 100644 --- a/tests/components/duotecno/test_config_flow.py +++ b/tests/components/duotecno/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -71,7 +71,7 @@ async def test_invalid(hass: HomeAssistant, test_side_effect, test_error): }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": test_error} with patch("duotecno.controller.PyDuotecno.connect"): @@ -83,7 +83,7 @@ async def test_invalid(hass: HomeAssistant, test_side_effect, test_error): "password": "test-password2", }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -105,5 +105,5 @@ async def test_already_setup(hass: HomeAssistant, mock_setup_entry: AsyncMock) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/dwd_weather_warnings/test_config_flow.py b/tests/components/dwd_weather_warnings/test_config_flow.py index 625532a4f04..119c029767a 100644 --- a/tests/components/dwd_weather_warnings/test_config_flow.py +++ b/tests/components/dwd_weather_warnings/test_config_flow.py @@ -6,51 +6,48 @@ from unittest.mock import patch import pytest from homeassistant.components.dwd_weather_warnings.const import ( - ADVANCE_WARNING_SENSOR, + CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, - CONF_REGION_NAME, - CURRENT_WARNING_SENSOR, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -DEMO_CONFIG_ENTRY: Final = { +DEMO_CONFIG_ENTRY_REGION: Final = { CONF_REGION_IDENTIFIER: "807111000", } -DEMO_YAML_CONFIGURATION: Final = { - CONF_NAME: "Unit Test", - CONF_REGION_NAME: "807111000", - CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], +DEMO_CONFIG_ENTRY_GPS: Final = { + CONF_REGION_DEVICE_TRACKER: "device_tracker.test_gps", } pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_create_entry(hass: HomeAssistant) -> None: - """Test that the full config flow works.""" +async def test_create_entry_region(hass: HomeAssistant) -> None: + """Test that the full config flow works for a region identifier.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", return_value=False, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) # Test for invalid region identifier. await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_identifier"} with patch( @@ -58,24 +55,107 @@ async def test_create_entry(hass: HomeAssistant) -> None: return_value=True, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) # Test for successfully created entry. await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "807111000" assert result["data"] == { CONF_REGION_IDENTIFIER: "807111000", } +async def test_create_entry_gps( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test that the full config flow works for a device tracker.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + # Test for missing registry entry error. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "entity_not_found"} + + # Test for missing device tracker error. + registry_entry = entity_registry.async_get_or_create( + "device_tracker", DOMAIN, "uuid", suggested_object_id="test_gps" + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "entity_not_found"} + + # Test for missing attribute error. + hass.states.async_set( + DEMO_CONFIG_ENTRY_GPS[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LONGITUDE: "7.610263"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "attribute_not_found"} + + # Test for invalid provided identifier. + hass.states.async_set( + DEMO_CONFIG_ENTRY_GPS[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LATITUDE: "50.180454", ATTR_LONGITUDE: "7.610263"}, + ) + + with patch( + "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_identifier"} + + # Test for successfully created entry. + with patch( + "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test_gps" + assert result["data"] == { + CONF_REGION_DEVICE_TRACKER: registry_entry.id, + } + + async def test_config_flow_already_configured(hass: HomeAssistant) -> None: """Test aborting, if the warncell ID / name is already configured during the config.""" entry = MockConfigEntry( domain=DOMAIN, - data=DEMO_CONFIG_ENTRY.copy(), - unique_id=DEMO_CONFIG_ENTRY[CONF_REGION_IDENTIFIER], + data=DEMO_CONFIG_ENTRY_REGION.copy(), + unique_id=DEMO_CONFIG_ENTRY_REGION[CONF_REGION_IDENTIFIER], ) entry.add_to_hass(hass) @@ -85,16 +165,47 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", return_value=True, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_config_flow_with_errors(hass: HomeAssistant) -> None: + """Test error scenarios during the configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + # Test error for empty input data. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_identifier"} + + # Test error for setting both options during configuration. + demo_input = DEMO_CONFIG_ENTRY_REGION.copy() + demo_input.update(DEMO_CONFIG_ENTRY_GPS.copy()) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=demo_input, + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "ambiguous_identifier"} diff --git a/tests/components/dwd_weather_warnings/test_init.py b/tests/components/dwd_weather_warnings/test_init.py index 6967f2ca6b1..bfd03b2fdd4 100644 --- a/tests/components/dwd_weather_warnings/test_init.py +++ b/tests/components/dwd_weather_warnings/test_init.py @@ -4,31 +4,45 @@ from typing import Final from homeassistant.components.dwd_weather_warnings.const import ( ADVANCE_WARNING_SENSOR, + CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, CURRENT_WARNING_SENSOR, DOMAIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + STATE_HOME, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -DEMO_CONFIG_ENTRY: Final = { +DEMO_IDENTIFIER_CONFIG_ENTRY: Final = { CONF_NAME: "Unit Test", CONF_REGION_IDENTIFIER: "807111000", CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], } +DEMO_TRACKER_CONFIG_ENTRY: Final = { + CONF_NAME: "Unit Test", + CONF_REGION_DEVICE_TRACKER: "device_tracker.test_gps", + CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], +} + async def test_load_unload_entry(hass: HomeAssistant) -> None: - """Test loading and unloading the integration.""" - entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) + """Test loading and unloading the integration with a region identifier based entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_IDENTIFIER_CONFIG_ENTRY) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.entry_id in hass.data[DOMAIN] assert await hass.config_entries.async_unload(entry.entry_id) @@ -36,3 +50,68 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED assert entry.entry_id not in hass.data[DOMAIN] + + +async def test_load_invalid_registry_entry(hass: HomeAssistant) -> None: + """Test loading the integration with an invalid registry entry ID.""" + INVALID_DATA = DEMO_TRACKER_CONFIG_ENTRY.copy() + INVALID_DATA[CONF_REGION_DEVICE_TRACKER] = "invalid_registry_id" + entry = MockConfigEntry(domain=DOMAIN, data=INVALID_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_missing_device_tracker(hass: HomeAssistant) -> None: + """Test loading the integration with a missing device tracker.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_missing_required_attribute(hass: HomeAssistant) -> None: + """Test loading the integration with a device tracker missing a required attribute.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + + hass.states.async_set( + DEMO_TRACKER_CONFIG_ENTRY[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LONGITUDE: "7.610263"}, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_valid_device_tracker( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test loading the integration with a valid device tracker based entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + entity_registry.async_get_or_create( + "device_tracker", + entry.domain, + "uuid", + suggested_object_id="test_gps", + config_entry=entry, + ) + + hass.states.async_set( + DEMO_TRACKER_CONFIG_ENTRY[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LATITUDE: "50.180454", ATTR_LONGITUDE: "7.610263"}, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 724cb616deb..2b56786e4e0 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -6,8 +6,10 @@ import pytest from homeassistant import config_entries from homeassistant.components import dynalite +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PORT from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.issue_registry import ( IssueSeverity, async_get as async_get_issue_registry, @@ -20,9 +22,9 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( ("first_con", "second_con", "exp_type", "exp_result", "exp_reason"), [ - (True, True, "create_entry", config_entries.ConfigEntryState.LOADED, ""), + (True, True, "create_entry", ConfigEntryState.LOADED, ""), (False, False, "abort", None, "cannot_connect"), - (True, False, "create_entry", config_entries.ConfigEntryState.SETUP_RETRY, ""), + (True, False, "create_entry", ConfigEntryState.SETUP_RETRY, ""), ], ) async def test_flow( @@ -86,7 +88,7 @@ async def test_existing(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={dynalite.CONF_HOST: host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -116,7 +118,7 @@ async def test_existing_update(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_dyn_dev().configure.call_count == 2 assert mock_dyn_dev().configure.mock_calls[1][1][0]["port"] == port2 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,8 +138,8 @@ async def test_two_entries(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={dynalite.CONF_HOST: host2}, ) - assert result["type"] == "create_entry" - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].state is ConfigEntryState.LOADED async def test_setup_user(hass): @@ -148,7 +150,7 @@ async def test_setup_user(hass): dynalite.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -161,8 +163,8 @@ async def test_setup_user(hass): {"host": host, "port": port}, ) - assert result["type"] == "create_entry" - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].state is ConfigEntryState.LOADED assert result["title"] == host assert result["data"] == { "host": host, @@ -188,5 +190,5 @@ async def test_setup_user_existing_host(hass): {"host": host, "port": 1234}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index e8f86154e67..9773a8b619e 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -8,6 +8,7 @@ from voluptuous.error import Invalid from homeassistant import config_entries from homeassistant.components.eafm import const from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_no_discovered_stations( @@ -18,7 +19,7 @@ async def test_flow_no_discovered_stations( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_stations" @@ -31,7 +32,7 @@ async def test_flow_invalid_station(hass: HomeAssistant, mock_get_stations) -> N result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with pytest.raises(Invalid): result = await hass.config_entries.flow.async_configure( @@ -53,14 +54,14 @@ async def test_flow_works( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.eafm.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"station": "My station"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My station" assert result["data"] == { "station": "L12345", diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index 380e1df5f37..082c4e08908 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -5,7 +5,7 @@ import datetime import aiohttp import pytest -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -37,7 +37,7 @@ async def async_setup_test_fixture(hass, mock_get_station, initial_value): entry.add_to_hass(hass) assert await async_setup_component(hass, "eafm", {}) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() async def poll(value): diff --git a/tests/components/easyenergy/test_config_flow.py b/tests/components/easyenergy/test_config_flow.py index 4e76d48b663..da7048793b3 100644 --- a/tests/components/easyenergy/test_config_flow.py +++ b/tests/components/easyenergy/test_config_flow.py @@ -17,7 +17,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert "flow_id" in result @@ -26,7 +26,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "easyEnergy" assert result2.get("data") == {} diff --git a/tests/components/ecobee/common.py b/tests/components/ecobee/common.py index 60f17c3618d..423b0eee320 100644 --- a/tests/components/ecobee/common.py +++ b/tests/components/ecobee/common.py @@ -4,14 +4,19 @@ from unittest.mock import patch from homeassistant.components.ecobee.const import CONF_REFRESH_TOKEN, DOMAIN from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def setup_platform(hass, platform) -> MockConfigEntry: +async def setup_platform( + hass: HomeAssistant, + platform: str, +) -> MockConfigEntry: """Set up the ecobee platform.""" mock_entry = MockConfigEntry( + title=DOMAIN, domain=DOMAIN, data={ CONF_API_KEY: "ABC123", @@ -22,7 +27,6 @@ async def setup_platform(hass, platform) -> MockConfigEntry: with patch("homeassistant.components.ecobee.const.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) - - await hass.async_block_till_done() + await hass.async_block_till_done() return mock_entry diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py index 952c2f3fba3..27d5a949c58 100644 --- a/tests/components/ecobee/conftest.py +++ b/tests/components/ecobee/conftest.py @@ -1,12 +1,13 @@ """Fixtures for tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from homeassistant.components.ecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN -from tests.common import load_fixture +from tests.common import load_fixture, load_json_object_fixture @pytest.fixture(autouse=True) @@ -23,11 +24,15 @@ def requests_mock_fixture(requests_mock): @pytest.fixture -def mock_ecobee(): +def mock_ecobee() -> Generator[None, MagicMock]: """Mock an Ecobee object.""" ecobee = MagicMock() ecobee.request_pin.return_value = True ecobee.refresh_tokens.return_value = True + ecobee.thermostats = load_json_object_fixture("ecobee-data.json", "ecobee")[ + "thermostatList" + ] + ecobee.get_thermostat = lambda index: ecobee.thermostats[index] ecobee.config = {ECOBEE_API_KEY: "mocked_key", ECOBEE_REFRESH_TOKEN: "mocked_token"} with patch("homeassistant.components.ecobee.Ecobee", return_value=ecobee): diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index d9406c20c3b..d8621bd8c4b 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -139,6 +139,81 @@ ] } ] + }, + { + "identifier": 8675307, + "name": "unknownEcobeeName", + "modelNumber": "unknownEcobeeModel", + "program": { + "climates": [ + { + "name": "Climate1", + "climateRef": "c1" + }, + { + "name": "Climate2", + "climateRef": "c2" + } + ], + "currentClimateRef": "c1" + }, + "runtime": { + "connected": true, + "actualTemperature": 300, + "desiredHeat": 400, + "desiredCool": 200, + "desiredFanMode": "on", + "desiredHumidity": 40 + }, + "settings": { + "hvacMode": "auto", + "heatStages": 1, + "coolStages": 1, + "fanMinOnTime": 10, + "heatCoolMinDelta": 50, + "holdAction": "nextTransition", + "ventilatorType": "none", + "ventilatorMinOnTimeHome": 20, + "ventilatorMinOnTimeAway": 10, + "isVentilatorTimerOn": false, + "hasHumidifier": true, + "humidifierMode": "manual", + "humidity": "30" + }, + "equipmentStatus": "fan", + "events": [ + { + "name": "Event1", + "running": true, + "type": "hold", + "holdClimateRef": "away", + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00" + } + ], + "remoteSensors": [ + { + "id": "rs:100", + "name": "Remote Sensor 1", + "type": "ecobee3_remote_sensor", + "code": "WKRP", + "inUse": false, + "capability": [ + { + "id": "1", + "type": "temperature", + "value": "782" + }, + { + "id": "2", + "type": "occupancy", + "value": "false" + } + ] + } + ] } ] } diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 0ec4f9cee68..46ca77025cc 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock import pytest +from homeassistant import const from homeassistant.components import climate from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.ecobee.climate import ( @@ -14,7 +15,6 @@ from homeassistant.components.ecobee.climate import ( 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 @@ -441,7 +441,7 @@ 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" + assert thermostat.preset_mode == "away" ecobee_fixture["events"][0]["endDate"] = "2999-01-01" assert thermostat.preset_mode == PRESET_AWAY_INDEFINITELY diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 91d9f848ffd..20d3dabb1ea 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN import pytest -from homeassistant import data_entry_flow from homeassistant.components.ecobee import config_flow from homeassistant.components.ecobee.const import ( CONF_REFRESH_TOKEN, @@ -14,6 +13,7 @@ from homeassistant.components.ecobee.const import ( ) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +27,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -38,7 +38,7 @@ async def test_user_step_without_user_input(hass: HomeAssistant) -> None: flow.hass.data[DATA_ECOBEE_CONFIG] = {} result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_pin_request_succeeds(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -72,7 +72,7 @@ async def test_pin_request_fails(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "pin_request_failed" @@ -93,7 +93,7 @@ async def test_token_request_succeeds(hass: HomeAssistant) -> None: result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", @@ -116,7 +116,7 @@ async def test_token_request_fails(hass: HomeAssistant) -> None: result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" assert result["errors"]["base"] == "token_request_failed" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass: HomeAssistant) -> result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -158,7 +158,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index 36b52c9c357..696ca3d6c0d 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.ecobee.humidifier import MODE_MANUAL, MODE_OFF from homeassistant.components.humidifier import ( ATTR_AVAILABLE_MODES, + ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -43,19 +44,18 @@ async def test_attributes(hass: HomeAssistant) -> None: state = hass.states.get(DEVICE_ID) assert state.state == STATE_ON - assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY - assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY - assert state.attributes.get(ATTR_HUMIDITY) == 40 - assert state.attributes.get(ATTR_AVAILABLE_MODES) == [ + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 15 + assert state.attributes[ATTR_MIN_HUMIDITY] == DEFAULT_MIN_HUMIDITY + assert state.attributes[ATTR_MAX_HUMIDITY] == DEFAULT_MAX_HUMIDITY + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.attributes[ATTR_AVAILABLE_MODES] == [ MODE_OFF, MODE_AUTO, MODE_MANUAL, ] - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ecobee" - assert state.attributes.get(ATTR_DEVICE_CLASS) == HumidifierDeviceClass.HUMIDIFIER - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) == HumidifierEntityFeature.MODES - ) + assert state.attributes[ATTR_FRIENDLY_NAME] == "ecobee" + assert state.attributes[ATTR_DEVICE_CLASS] == HumidifierDeviceClass.HUMIDIFIER + assert state.attributes[ATTR_SUPPORTED_FEATURES] == HumidifierEntityFeature.MODES async def test_turn_on(hass: HomeAssistant) -> None: diff --git a/tests/components/ecobee/test_notify.py b/tests/components/ecobee/test_notify.py new file mode 100644 index 00000000000..c66f04c752a --- /dev/null +++ b/tests/components/ecobee/test_notify.py @@ -0,0 +1,57 @@ +"""Test Ecobee notify service.""" + +from unittest.mock import MagicMock + +from homeassistant.components.ecobee import DOMAIN +from homeassistant.components.notify import ( + DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .common import setup_platform + +THERMOSTAT_ID = 0 + + +async def test_notify_entity_service( + hass: HomeAssistant, + mock_ecobee: MagicMock, +) -> None: + """Test the notify entity service.""" + await setup_platform(hass, NOTIFY_DOMAIN) + + entity_id = "notify.ecobee" + state = hass.states.get(entity_id) + assert state is not None + assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE) + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + service_data={"entity_id": entity_id, "message": "It is too cold!"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_ecobee.send_message.assert_called_with(THERMOSTAT_ID, "It is too cold!") + + +async def test_legacy_notify_service( + hass: HomeAssistant, + mock_ecobee: MagicMock, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the legacy notify service.""" + await setup_platform(hass, NOTIFY_DOMAIN) + + assert hass.services.has_service(NOTIFY_DOMAIN, DOMAIN) + await hass.services.async_call( + NOTIFY_DOMAIN, + DOMAIN, + service_data={"message": "It is too cold!", "target": THERMOSTAT_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_ecobee.send_message.assert_called_with(THERMOSTAT_ID, "It is too cold!") + mock_ecobee.send_message.reset_mock() + assert len(issue_registry.issues) == 1 diff --git a/tests/components/ecobee/test_repairs.py b/tests/components/ecobee/test_repairs.py new file mode 100644 index 00000000000..19fdc6f7bba --- /dev/null +++ b/tests/components/ecobee/test_repairs.py @@ -0,0 +1,79 @@ +"""Test repairs for Ecobee integration.""" + +from http import HTTPStatus +from unittest.mock import MagicMock + +from homeassistant.components.ecobee import DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .common import setup_platform + +from tests.typing import ClientSessionGenerator + +THERMOSTAT_ID = 0 + + +async def test_ecobee_repair_flow( + hass: HomeAssistant, + mock_ecobee: MagicMock, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the ecobee notify service repair flow is triggered.""" + await setup_platform(hass, NOTIFY_DOMAIN) + await async_process_repairs_platforms(hass) + + http_client = await hass_client() + + # Simulate legacy service being used + assert hass.services.has_service(NOTIFY_DOMAIN, DOMAIN) + await hass.services.async_call( + NOTIFY_DOMAIN, + DOMAIN, + service_data={"message": "It is too cold!", "target": THERMOSTAT_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_ecobee.send_message.assert_called_with(THERMOSTAT_ID, "It is too cold!") + mock_ecobee.send_message.reset_mock() + + # Assert the issue is present + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + assert len(issue_registry.issues) == 1 + + url = RepairsFlowIndexView.url + resp = await http_client.post( + url, json={"handler": DOMAIN, "issue_id": "migrate_notify"} + ) + 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 http_client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["type"] == "create_entry" + # Test confirm step in repair flow + await hass.async_block_till_done() + + # Assert the issue is no longer present + assert not issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + assert len(issue_registry.issues) == 0 diff --git a/tests/components/ecoforest/test_config_flow.py b/tests/components/ecoforest/test_config_flow.py index 95c63a2515d..ae18960c7f9 100644 --- a/tests/components/ecoforest/test_config_flow.py +++ b/tests/components/ecoforest/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -34,7 +34,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "result" in result assert result["result"].unique_id == "1234" assert result["title"] == "Ecoforest 1234" @@ -53,7 +53,7 @@ async def test_form_device_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -66,7 +66,7 @@ async def test_form_device_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +100,7 @@ async def test_flow_fails( config, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": message} with patch( @@ -113,4 +113,4 @@ async def test_flow_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py index 7647b77e0a6..2ef10c1bd41 100644 --- a/tests/components/econet/test_config_flow.py +++ b/tests/components/econet/test_config_flow.py @@ -20,7 +20,7 @@ async def test_bad_credentials(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -38,7 +38,7 @@ async def test_bad_credentials(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "invalid_auth", @@ -51,7 +51,7 @@ async def test_generic_error_from_library(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -69,7 +69,7 @@ async def test_generic_error_from_library(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "cannot_connect", @@ -82,7 +82,7 @@ async def test_auth_worked(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -100,7 +100,7 @@ async def test_auth_worked(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: "admin@localhost.com", CONF_PASSWORD: "password0", @@ -120,7 +120,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -138,5 +138,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index 816551f7e6a..d250a60a35f 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -1,4 +1,96 @@ # serializer version: 1 +# name: test_buttons[5xu9h3][button.goat_g1_reset_blade_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.goat_g1_reset_blade_lifespan', + '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': 'Reset blade lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_blade', + 'unique_id': '8516fbb1-17f1-4194-0000000_reset_lifespan_blade', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[5xu9h3][button.goat_g1_reset_blade_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Reset blade lifespan', + }), + 'context': , + 'entity_id': 'button.goat_g1_reset_blade_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[5xu9h3][button.goat_g1_reset_lens_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.goat_g1_reset_lens_brush_lifespan', + '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': 'Reset lens brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_lens_brush', + 'unique_id': '8516fbb1-17f1-4194-0000000_reset_lifespan_lens_brush', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[5xu9h3][button.goat_g1_reset_lens_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Reset lens brush lifespan', + }), + 'context': , + 'entity_id': 'button.goat_g1_reset_lens_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- # name: test_buttons[yna5x1][button.ozmo_950_relocate:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index b35310158f2..e2cee3d410f 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -1,5 +1,583 @@ # serializer version: 1 -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_area_cleaned:entity-registry] +# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned: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.goat_g1_area_cleaned', + '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': 'Area cleaned', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stats_area', + 'unique_id': '8516fbb1-17f1-4194-0000000_stats_area', + 'unit_of_measurement': 'm²', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Area cleaned', + 'unit_of_measurement': 'm²', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_area_cleaned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_battery: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.goat_g1_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '8516fbb1-17f1-4194-0000000_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_battery:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Goat G1 Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_blade_lifespan: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.goat_g1_blade_lifespan', + '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': 'Blade lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_blade', + 'unique_id': '8516fbb1-17f1-4194-0000000_lifespan_blade', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_blade_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Blade lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_blade_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_cleaning_duration: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.goat_g1_cleaning_duration', + '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': None, + 'original_name': 'Cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stats_time', + 'unique_id': '8516fbb1-17f1-4194-0000000_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Goat G1 Cleaning duration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.goat_g1_cleaning_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_error: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.goat_g1_error', + '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': 'Error', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'error', + 'unique_id': '8516fbb1-17f1-4194-0000000_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_error:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'description': 'NoError: Robot is operational', + 'friendly_name': 'Goat G1 Error', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_ip_address: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.goat_g1_ip_address', + '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': 'IP address', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_ip', + 'unique_id': '8516fbb1-17f1-4194-0000000_network_ip', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_ip_address:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 IP address', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_ip_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '192.168.0.10', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_lens_brush_lifespan: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.goat_g1_lens_brush_lifespan', + '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': 'Lens brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_lens_brush', + 'unique_id': '8516fbb1-17f1-4194-0000000_lifespan_lens_brush', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_lens_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Lens brush lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_lens_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned: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.goat_g1_total_area_cleaned', + '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': 'Total area cleaned', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_area', + 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area', + 'unit_of_measurement': 'm²', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Total area cleaned', + 'state_class': , + 'unit_of_measurement': 'm²', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_total_area_cleaned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration: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.goat_g1_total_cleaning_duration', + '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': None, + 'original_name': 'Total cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_time', + 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Goat G1 Total cleaning duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.goat_g1_total_cleaning_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.000', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings: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.goat_g1_total_cleanings', + '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': 'Total cleanings', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_cleanings', + 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_cleanings', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Total cleanings', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.goat_g1_total_cleanings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_rssi: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.goat_g1_wi_fi_rssi', + '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 RSSI', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rssi', + 'unique_id': '8516fbb1-17f1-4194-0000000_network_rssi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_rssi:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Wi-Fi RSSI', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_wi_fi_rssi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-62', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_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.goat_g1_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': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_ssid', + 'unique_id': '8516fbb1-17f1-4194-0000000_network_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Testnetwork', + }) +# --- +# name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -32,7 +610,7 @@ 'unit_of_measurement': 'm²', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_area_cleaned:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Area cleaned', @@ -46,7 +624,7 @@ 'state': '10', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_battery:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_battery:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -79,7 +657,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_battery:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_battery:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -94,7 +672,7 @@ 'state': '100', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_cleaning_duration:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -130,7 +708,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_cleaning_duration:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -145,7 +723,7 @@ 'state': '5.0', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_error:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -178,7 +756,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_error:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'description': 'NoError: Robot is operational', @@ -192,7 +770,7 @@ 'state': '0', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_filter_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_filter_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -225,7 +803,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_filter_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_filter_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Filter lifespan', @@ -239,7 +817,7 @@ 'state': '56', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_ip_address:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_ip_address:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -272,7 +850,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_ip_address:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_ip_address:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 IP address', @@ -285,7 +863,7 @@ 'state': '192.168.0.10', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_main_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -318,7 +896,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_main_brush_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Main brush lifespan', @@ -332,7 +910,7 @@ 'state': '80', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_side_brushes_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -365,7 +943,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_side_brushes_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Side brushes lifespan', @@ -379,7 +957,7 @@ 'state': '40', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -414,7 +992,7 @@ 'unit_of_measurement': 'm²', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Total area cleaned', @@ -429,7 +1007,7 @@ 'state': '60', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleaning_duration:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -467,7 +1045,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleaning_duration:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -483,7 +1061,7 @@ 'state': '40.000', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleanings:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -518,7 +1096,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleanings:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Total cleanings', @@ -532,7 +1110,7 @@ 'state': '123', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_rssi:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -565,7 +1143,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_rssi:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Wi-Fi RSSI', @@ -578,7 +1156,7 @@ 'state': '-62', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_ssid:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -611,7 +1189,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_ssid:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Wi-Fi SSID', diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 8e583e6342b..277983eb0c5 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -48,8 +48,21 @@ def platforms() -> Platform | list[Platform]: ), ], ), + ( + "5xu9h3", + [ + ( + "button.goat_g1_reset_blade_lifespan", + ResetLifeSpan(LifeSpan.BLADE), + ), + ( + "button.goat_g1_reset_lens_brush_lifespan", + ResetLifeSpan(LifeSpan.LENS_BRUSH), + ), + ], + ), ], - ids=["yna5x1"], + ids=["yna5x1", "5xu9h3"], ) async def test_buttons( hass: HomeAssistant, @@ -98,6 +111,13 @@ async def test_buttons( "button.ozmo_950_reset_side_brushes_lifespan", ], ), + ( + "5xu9h3", + [ + "button.goat_g1_reset_blade_lifespan", + "button.goat_g1_reset_lens_brush_lifespan", + ], + ), ], ) async def test_disabled_by_default_buttons( diff --git a/tests/components/ecovacs/test_config_flow.py b/tests/components/ecovacs/test_config_flow.py index 6bd30c3a201..0a161f88baa 100644 --- a/tests/components/ecovacs/test_config_flow.py +++ b/tests/components/ecovacs/test_config_flow.py @@ -49,7 +49,7 @@ async def _test_user_flow( context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert not result["errors"] @@ -71,7 +71,7 @@ async def _test_user_flow_show_advanced_options( context={"source": SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -80,7 +80,7 @@ async def _test_user_flow_show_advanced_options( user_input=user_input_user or {}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert not result["errors"] @@ -131,7 +131,7 @@ async def test_user_flow( hass, **test_fn_args, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_data[CONF_USERNAME] assert result["data"] == entry_data mock_setup_entry.assert_called() @@ -214,7 +214,7 @@ async def test_user_flow_raise_error( hass, **test_fn_args, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": reason_rest} mock_authenticator_authenticate.assert_called() @@ -229,7 +229,7 @@ async def test_user_flow_raise_error( result["flow_id"], user_input=user_input_auth, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == errors_mqtt(user_input_auth) mock_authenticator_authenticate.assert_called() @@ -243,7 +243,7 @@ async def test_user_flow_raise_error( result["flow_id"], user_input=user_input_auth, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_data[CONF_USERNAME] assert result["data"] == entry_data mock_setup_entry.assert_called() @@ -269,7 +269,7 @@ async def test_user_flow_self_hosted_error( user_input_user=_USER_STEP_SELF_HOSTED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == { CONF_OVERRIDE_REST_URL: "invalid_url_schema_override_rest_url", @@ -297,7 +297,7 @@ async def test_user_flow_self_hosted_error( assert ssl_context.verify_mode == ssl.CERT_NONE assert ssl_context.check_hostname is False - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == data[CONF_USERNAME] assert result["data"] == data mock_setup_entry.assert_called() @@ -320,7 +320,7 @@ async def test_import_flow( ) mock_authenticator_authenticate.assert_called() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == VALID_ENTRY_DATA_CLOUD[CONF_USERNAME] assert result["data"] == VALID_ENTRY_DATA_CLOUD assert (HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}") in issue_registry.issues @@ -340,7 +340,7 @@ async def test_import_flow_already_configured( context={"source": SOURCE_IMPORT}, data=IMPORT_DATA.copy(), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert (HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}") in issue_registry.issues @@ -374,7 +374,7 @@ async def test_import_flow_error( }, data=IMPORT_DATA.copy(), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert ( DOMAIN, @@ -410,7 +410,7 @@ async def test_import_flow_invalid_data( }, data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert ( DOMAIN, diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py index 0e7adaad954..104a3bfc69e 100644 --- a/tests/components/ecovacs/test_event.py +++ b/tests/components/ecovacs/test_event.py @@ -76,7 +76,7 @@ async def test_last_job( await notify_and_wait( hass, event_bus, - ReportStatsEvent(0, 1, "spotArea", "3", CleanJobStatus.MANUAL_STOPPED, [1]), + ReportStatsEvent(0, 1, "spotArea", "3", CleanJobStatus.MANUALLY_STOPPED, [1]), ) assert (state := hass.states.get(state.entity_id)) diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 7780b86d714..c27da2196b1 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -122,7 +122,7 @@ async def test_devices_in_dr( ("device_fixture", "entities"), [ ("yna5x1", 26), - ("5xu9h3", 20), + ("5xu9h3", 24), ], ) async def test_all_entities_loaded( diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 7ff4ab3f009..5b8bf18e1d8 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -69,7 +69,25 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): "sensor.ozmo_950_error", ], ), + ( + "5xu9h3", + [ + "sensor.goat_g1_area_cleaned", + "sensor.goat_g1_cleaning_duration", + "sensor.goat_g1_total_area_cleaned", + "sensor.goat_g1_total_cleaning_duration", + "sensor.goat_g1_total_cleanings", + "sensor.goat_g1_battery", + "sensor.goat_g1_ip_address", + "sensor.goat_g1_wi_fi_rssi", + "sensor.goat_g1_wi_fi_ssid", + "sensor.goat_g1_blade_lifespan", + "sensor.goat_g1_lens_brush_lifespan", + "sensor.goat_g1_error", + ], + ), ], + ids=["yna5x1", "5xu9h3"], ) async def test_sensors( hass: HomeAssistant, @@ -111,7 +129,17 @@ async def test_sensors( "sensor.ozmo_950_wi_fi_ssid", ], ), + ( + "5xu9h3", + [ + "sensor.goat_g1_error", + "sensor.goat_g1_ip_address", + "sensor.goat_g1_wi_fi_rssi", + "sensor.goat_g1_wi_fi_ssid", + ], + ), ], + ids=["yna5x1", "5xu9h3"], ) async def test_disabled_by_default_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, entity_ids: list[str] diff --git a/tests/components/ecowitt/test_config_flow.py b/tests/components/ecowitt/test_config_flow.py index 24a45e2d31b..a2054c1282d 100644 --- a/tests/components/ecowitt/test_config_flow.py +++ b/tests/components/ecowitt/test_config_flow.py @@ -16,7 +16,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -29,7 +29,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Ecowitt" assert result2["data"] == { "webhook_id": result2["description_placeholders"]["path"].split("/")[-1], diff --git a/tests/components/edl21/test_config_flow.py b/tests/components/edl21/test_config_flow.py index 030ff7ae63e..97ad1464d77 100644 --- a/tests/components/edl21/test_config_flow.py +++ b/tests/components/edl21/test_config_flow.py @@ -22,7 +22,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -30,7 +30,7 @@ async def test_show_form(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_TITLE assert result["data"][CONF_SERIAL_PORT] == VALID_CONFIG[CONF_SERIAL_PORT] @@ -49,5 +49,5 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index 3a7529da395..9a66c42bc9a 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -24,14 +24,14 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == HID @@ -44,7 +44,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -56,7 +56,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -68,7 +68,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -87,7 +87,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" new_conf = {CONF_API_KEY: "1234567890"} @@ -95,6 +95,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py index 5c72e1a5cfd..151cd50fbc6 100644 --- a/tests/components/efergy/test_init.py +++ b/tests/components/efergy/test_init.py @@ -16,7 +16,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test unload.""" entry = await init_integration(hass, aioclient_mock) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -32,7 +32,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: efergymock.side_effect = (exceptions.ConnectError, exceptions.DataError) await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -43,7 +43,7 @@ async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None: efergymock.side_effect = exceptions.InvalidAuth await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/electrasmart/test_config_flow.py b/tests/components/electrasmart/test_config_flow.py index 957c140862f..cf0d1b5ab15 100644 --- a/tests/components/electrasmart/test_config_flow.py +++ b/tests/components/electrasmart/test_config_flow.py @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant): data={CONF_PHONE_NUMBER: "0521234567"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_OTP @@ -73,7 +73,7 @@ async def test_one_time_password(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP: "1234"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_one_time_password_api_error(hass: HomeAssistant): @@ -99,7 +99,7 @@ async def test_one_time_password_api_error(hass: HomeAssistant): result["flow_id"], {CONF_OTP: "1234"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect(hass: HomeAssistant): @@ -115,7 +115,7 @@ async def test_cannot_connect(hass: HomeAssistant): context={"source": config_entries.SOURCE_USER}, data={CONF_PHONE_NUMBER: "0521234567"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -138,7 +138,7 @@ async def test_invalid_phone_number(hass: HomeAssistant): data={CONF_PHONE_NUMBER: "0521234567"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "invalid_phone_number"} @@ -171,6 +171,6 @@ async def test_invalid_auth(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP: "1234"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_OTP assert result["errors"] == {CONF_OTP: "invalid_auth"} diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 0a1d32f0ec0..8052ae5e129 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator from time import time from unittest.mock import AsyncMock, patch -import zoneinfo from electrickiwi_api.model import AccountBalance, Hop, HopIntervals import pytest @@ -24,8 +23,6 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" REDIRECT_URI = "https://example.com/auth/external/callback" -TZ_NAME = "Pacific/Auckland" -TIMEZONE = zoneinfo.ZoneInfo(TZ_NAME) YieldFixture = Generator[AsyncMock, None, None] ComponentSetup = Callable[[], Awaitable[bool]] @@ -63,7 +60,7 @@ def component_setup( @pytest.fixture(name="config_entry") def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Create mocked config entry.""" - entry = MockConfigEntry( + return MockConfigEntry( title="Electric Kiwi", domain=DOMAIN, data={ @@ -79,7 +76,6 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: }, unique_id=DOMAIN, ) - return entry @pytest.fixture diff --git a/tests/components/electric_kiwi/test_config_flow.py b/tests/components/electric_kiwi/test_config_flow.py index d91936eeebf..d74abab7692 100644 --- a/tests/components/electric_kiwi/test_config_flow.py +++ b/tests/components/electric_kiwi/test_config_flow.py @@ -49,7 +49,7 @@ async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" diff --git a/tests/components/electric_kiwi/test_sensor.py b/tests/components/electric_kiwi/test_sensor.py index f91e4d9c58c..a247497b263 100644 --- a/tests/components/electric_kiwi/test_sensor.py +++ b/tests/components/electric_kiwi/test_sensor.py @@ -2,6 +2,7 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock +import zoneinfo from freezegun import freeze_time import pytest @@ -19,10 +20,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry import homeassistant.util.dt as dt_util -from .conftest import TIMEZONE, ComponentSetup, YieldFixture +from .conftest import ComponentSetup, YieldFixture from tests.common import MockConfigEntry +DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE +TEST_TZ_NAME = "Pacific/Auckland" +TEST_TIMEZONE = zoneinfo.ZoneInfo(TEST_TZ_NAME) + + +@pytest.fixture(autouse=True) +def restore_timezone(): + """Restore default timezone.""" + yield + + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) + @pytest.mark.parametrize( ("sensor", "sensor_state"), @@ -124,8 +137,8 @@ async def test_check_and_move_time(ek_api: AsyncMock) -> None: """Test correct time is returned depending on time of day.""" hop = await ek_api(Mock()).get_hop() - test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TIMEZONE) - dt_util.set_default_time_zone(TIMEZONE) + test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TEST_TIMEZONE) + dt_util.set_default_time_zone(TEST_TIMEZONE) with freeze_time(test_time): value = _check_and_move_time(hop, "4:00 PM") diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index def12307107..6da99241b64 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -29,14 +29,14 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -66,7 +66,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {"serial_number": "CN11A1A00001"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 @@ -78,7 +78,7 @@ async def test_full_zeroconf_flow_implementation( result["flow_id"], user_input={} ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -97,7 +97,7 @@ async def test_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -123,7 +123,7 @@ async def test_zeroconf_connection_error( ) assert result.get("reason") == "cannot_connect" - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT @pytest.mark.usefixtures("mock_elgato") @@ -138,7 +138,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -162,7 +162,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -183,7 +183,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -212,7 +212,7 @@ async def test_zeroconf_during_onboarding( ), ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 592efc16b5e..e56bb5f4699 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -53,7 +53,7 @@ async def test_discovery_ignored_entry(hass: HomeAssistant) -> None: data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +66,7 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -95,7 +95,7 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -123,7 +123,7 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -152,7 +152,7 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -180,7 +180,7 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -209,7 +209,7 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -237,7 +237,7 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -264,7 +264,7 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -285,7 +285,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured( ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -304,7 +304,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "address_already_configured" @@ -317,7 +317,7 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -350,7 +350,7 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -375,7 +375,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -411,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1" assert result3["data"] == { "auto_configure": True, @@ -436,7 +436,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -472,7 +472,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -495,7 +495,7 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -524,7 +524,7 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -546,7 +546,7 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -573,7 +573,7 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { "auto_configure": True, @@ -595,7 +595,7 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -622,7 +622,7 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -667,7 +667,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -703,7 +703,7 @@ async def test_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -730,7 +730,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -757,7 +757,7 @@ async def test_form_invalid_auth_no_password(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -807,7 +807,7 @@ async def test_form_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["data"] == { @@ -877,7 +877,7 @@ async def test_form_import_device_discovered(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { @@ -929,7 +929,7 @@ async def test_form_import_non_secure_device_discovered(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { @@ -973,7 +973,7 @@ async def test_form_import_non_secure_non_stanadard_port_device_discovered( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { @@ -1007,7 +1007,7 @@ async def test_form_import_non_secure_device_discovered_invalid_auth( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -1051,7 +1051,7 @@ async def test_form_import_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -1079,7 +1079,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "cc:cc:cc:cc:cc:cc" @@ -1108,7 +1108,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MOCK_MAC @@ -1124,7 +1124,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), _patch_elk(): @@ -1134,7 +1134,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_elk(): @@ -1148,7 +1148,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -1163,7 +1163,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1189,7 +1189,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1213,7 +1213,7 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1239,7 +1239,7 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1271,7 +1271,7 @@ async def test_discovered_by_discovery_url_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1284,7 +1284,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1310,7 +1310,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1334,7 +1334,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1359,7 +1359,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1389,7 +1389,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1412,7 +1412,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1434,7 +1434,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -1450,7 +1450,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1483,7 +1483,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -1502,7 +1502,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1532,7 +1532,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeefe" assert result3["data"] == { "auto_configure": True, @@ -1551,7 +1551,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -1575,7 +1575,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { "auto_configure": True, @@ -1599,7 +1599,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1612,7 +1612,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert not result["errors"] assert result2["step_id"] == "discovered_connection" with ( @@ -1636,7 +1636,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -1655,7 +1655,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1686,7 +1686,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeefe" assert result3["data"] == { "auto_configure": True, @@ -1705,7 +1705,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -1731,7 +1731,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { "auto_configure": True, diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index 6782b3f9b7a..c00de2003c2 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.elmax.const import ( CONF_ELMAX_MODE, @@ -23,6 +23,7 @@ from homeassistant.components.elmax.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_DIRECT_CERT, @@ -89,7 +90,7 @@ async def test_show_menu(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_mode" @@ -116,7 +117,7 @@ async def test_direct_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_direct_show_form(hass: HomeAssistant) -> None: @@ -134,7 +135,7 @@ async def test_direct_show_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == CONF_ELMAX_MODE_DIRECT assert result["errors"] is None @@ -168,7 +169,7 @@ async def test_cloud_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_form_setup_api_not_supported(hass): @@ -178,7 +179,7 @@ async def test_zeroconf_form_setup_api_not_supported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO_NOT_SUPPORTED, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -189,7 +190,7 @@ async def test_zeroconf_discovery(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_setup" assert result["errors"] is None @@ -206,7 +207,7 @@ async def test_zeroconf_setup_show_form(hass): result["flow_id"], ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_setup" @@ -227,7 +228,7 @@ async def test_zeroconf_setup(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_already_configured(hass): @@ -252,7 +253,7 @@ async def test_zeroconf_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -283,7 +284,7 @@ async def test_zeroconf_panel_changed_ip(hass): ) # Expect we abort the configuration as "already configured" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Expect the panel ip has been updated. @@ -330,7 +331,7 @@ async def test_one_config_allowed_cloud(hass: HomeAssistant) -> None: CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -355,7 +356,7 @@ async def test_cloud_invalid_credentials(hass: HomeAssistant) -> None: }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD - assert login_result["type"] == data_entry_flow.FlowResultType.FORM + assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} @@ -380,7 +381,7 @@ async def test_cloud_connection_error(hass: HomeAssistant) -> None: }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD - assert login_result["type"] == data_entry_flow.FlowResultType.FORM + assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} @@ -407,7 +408,7 @@ async def test_direct_connection_error(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == CONF_ELMAX_MODE_DIRECT - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "network_error"} @@ -434,7 +435,7 @@ async def test_direct_wrong_panel_code(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == CONF_ELMAX_MODE_DIRECT - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -466,7 +467,7 @@ async def test_unhandled_error(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -499,7 +500,7 @@ async def test_invalid_pin(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -525,7 +526,7 @@ async def test_no_online_panel(hass: HomeAssistant) -> None: }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD - assert login_result["type"] == data_entry_flow.FlowResultType.FORM + assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} @@ -557,7 +558,7 @@ async def test_show_reauth(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -602,7 +603,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert result["reason"] == "reauth_successful" @@ -650,7 +651,7 @@ async def test_reauth_panel_disappeared(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} @@ -696,7 +697,7 @@ async def test_reauth_invalid_pin(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -742,5 +743,5 @@ async def test_reauth_bad_login(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/elvia/test_config_flow.py b/tests/components/elvia/test_config_flow.py index 2fda29217c4..34edac499ac 100644 --- a/tests/components/elvia/test_config_flow.py +++ b/tests/components/elvia/test_config_flow.py @@ -27,7 +27,7 @@ async def test_single_metering_point( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -42,7 +42,7 @@ async def test_single_metering_point( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1234" assert result["data"] == { CONF_API_TOKEN: TEST_API_TOKEN, @@ -60,7 +60,7 @@ async def test_multiple_metering_points( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -80,7 +80,7 @@ async def test_multiple_metering_points( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_meter" result = await hass.config_entries.flow.async_configure( @@ -91,7 +91,7 @@ async def test_multiple_metering_points( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "5678" assert result["data"] == { CONF_API_TOKEN: TEST_API_TOKEN, @@ -109,7 +109,7 @@ async def test_no_metering_points( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -124,7 +124,7 @@ async def test_no_metering_points( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_metering_points" assert len(mock_setup_entry.mock_calls) == 0 @@ -139,7 +139,7 @@ async def test_bad_data( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -154,7 +154,7 @@ async def test_bad_data( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_metering_points" assert len(mock_setup_entry.mock_calls) == 0 @@ -175,7 +175,7 @@ async def test_abort_when_metering_point_id_exist( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -190,7 +190,7 @@ async def test_abort_when_metering_point_id_exist( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "metering_point_id_already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -227,7 +227,7 @@ async def test_form_exceptions( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} # Simulate that the user gives up and closes the window... diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index 07809a83d89..e77ebcc08b0 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components import dhcp from homeassistant.components.emonitor.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -53,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Emonitor DDEEFF" assert result2["data"] == { "host": "1.2.3.4", @@ -78,7 +79,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -99,7 +100,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -117,7 +118,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -134,7 +135,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Emonitor DDEEFF" assert result2["data"] == { "host": "1.2.3.4", @@ -156,7 +157,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -181,7 +182,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -198,7 +199,7 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -219,5 +220,5 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 700adbf0039..45cb83b4fea 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -3,6 +3,7 @@ from homeassistant import config_entries from homeassistant.components.emulated_roku import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +16,7 @@ async def test_flow_works(hass: HomeAssistant, mock_get_source_ip) -> None: data={"name": "Emulated Roku Test", "listen_port": 8060}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Emulated Roku Test" assert result["data"] == {"name": "Emulated Roku Test", "listen_port": 8060} @@ -34,5 +35,5 @@ async def test_flow_already_registered_entry( data={"name": "Emulated Roku Test", "listen_port": 8062}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/energenie_power_sockets/__init__.py b/tests/components/energenie_power_sockets/__init__.py new file mode 100644 index 00000000000..8397567ef82 --- /dev/null +++ b/tests/components/energenie_power_sockets/__init__.py @@ -0,0 +1 @@ +"""Tests for Energenie-Power-Sockets (EGPS) integration.""" diff --git a/tests/components/energenie_power_sockets/conftest.py b/tests/components/energenie_power_sockets/conftest.py new file mode 100644 index 00000000000..f119c0008f7 --- /dev/null +++ b/tests/components/energenie_power_sockets/conftest.py @@ -0,0 +1,83 @@ +"""Configure tests for Energenie-Power-Sockets.""" + +from collections.abc import Generator +from typing import Final +from unittest.mock import MagicMock, patch + +from pyegps.fakes.powerstrip import FakePowerStrip +import pytest + +from homeassistant.components.energenie_power_sockets.const import ( + CONF_DEVICE_API_ID, + DOMAIN, +) +from homeassistant.const import CONF_NAME + +from tests.common import MockConfigEntry + +DEMO_CONFIG_DATA: Final = { + CONF_NAME: "Unit Test", + CONF_DEVICE_API_ID: "DYPS:00:11:22", +} + + +@pytest.fixture +def demo_config_data() -> dict: + """Return valid user input.""" + return {CONF_DEVICE_API_ID: DEMO_CONFIG_DATA[CONF_DEVICE_API_ID]} + + +@pytest.fixture +def valid_config_entry() -> MockConfigEntry: + """Return a valid egps config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=DEMO_CONFIG_DATA, + unique_id=DEMO_CONFIG_DATA[CONF_DEVICE_API_ID], + ) + + +@pytest.fixture(name="pyegps_device_mock") +def get_pyegps_device_mock() -> MagicMock: + """Fixture for a mocked FakePowerStrip.""" + + fkObj = FakePowerStrip( + devId=DEMO_CONFIG_DATA[CONF_DEVICE_API_ID], number_of_sockets=4 + ) + fkObj.release = lambda: True + fkObj._status = [0, 1, 0, 1] + + usb_device_mock = MagicMock(wraps=fkObj) + usb_device_mock.get_device_type.return_value = "PowerStrip" + usb_device_mock.numberOfSockets = 4 + usb_device_mock.device_id = DEMO_CONFIG_DATA[CONF_DEVICE_API_ID] + usb_device_mock.manufacturer = "Energenie" + usb_device_mock.name = "MockedUSBDevice" + + return usb_device_mock + + +@pytest.fixture(name="mock_get_device") +def patch_get_device(pyegps_device_mock: MagicMock) -> Generator[MagicMock, None, None]: + """Fixture to patch the `get_device` api method.""" + with ( + patch("homeassistant.components.energenie_power_sockets.get_device") as m1, + patch( + "homeassistant.components.energenie_power_sockets.config_flow.get_device", + new=m1, + ) as mock, + ): + mock.return_value = pyegps_device_mock + yield mock + + +@pytest.fixture(name="mock_search_for_devices") +def patch_search_devices( + pyegps_device_mock: MagicMock, +) -> Generator[MagicMock, None, None]: + """Fixture to patch the `search_for_devices` api method.""" + with patch( + "homeassistant.components.energenie_power_sockets.config_flow.search_for_devices", + return_value=[pyegps_device_mock], + ) as mock: + yield mock diff --git a/tests/components/energenie_power_sockets/snapshots/test_switch.ambr b/tests/components/energenie_power_sockets/snapshots/test_switch.ambr new file mode 100644 index 00000000000..d462d6ca6d4 --- /dev/null +++ b/tests/components/energenie_power_sockets/snapshots/test_switch.ambr @@ -0,0 +1,189 @@ +# serializer version: 1 +# name: test_switch_setup[mockedusbdevice_socket_0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 0', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_0].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.mockedusbdevice_socket_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 0', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 1', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_1].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.mockedusbdevice_socket_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': 'Socket 1', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 2', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_2].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.mockedusbdevice_socket_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': 'Socket 2', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_3] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 3', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_3].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.mockedusbdevice_socket_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': 'Socket 3', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_3', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/energenie_power_sockets/test_config_flow.py b/tests/components/energenie_power_sockets/test_config_flow.py new file mode 100644 index 00000000000..aee26438629 --- /dev/null +++ b/tests/components/energenie_power_sockets/test_config_flow.py @@ -0,0 +1,140 @@ +"""Tests for Energenie-Power-Sockets config flow.""" + +from unittest.mock import MagicMock + +from pyegps.exceptions import UsbError + +from homeassistant.components.energenie_power_sockets.const import ( + CONF_DEVICE_API_ID, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_user_flow( + hass: HomeAssistant, + demo_config_data: dict, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow initialized by the user.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] is FlowResultType.FORM + assert not result1["errors"] + + # check with valid data + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], user_input=demo_config_data + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + + +async def test_user_flow_already_exists( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test the flow when device has been already configured.""" + valid_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_DEVICE_API_ID: valid_config_entry.data[CONF_DEVICE_API_ID]}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_user_flow_no_new_device( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test the flow when the found device has been already included.""" + valid_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=None, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_device" + + +async def test_user_flow_no_device_found( + hass: HomeAssistant, + demo_config_data: dict, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow when no device is found.""" + + mock_search_for_devices.return_value = [] + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] is FlowResultType.ABORT + assert result1["reason"] == "no_device" + + +async def test_user_flow_device_not_found( + hass: HomeAssistant, + demo_config_data: dict, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow when the given device_id does not match any found devices.""" + + mock_get_device.return_value = None + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] is FlowResultType.FORM + assert not result1["errors"] + + # check with valid data + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], user_input=demo_config_data + ) + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "device_not_found" + + +async def test_user_flow_no_usb_access( + hass: HomeAssistant, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow when USB devices can't be accessed.""" + + mock_get_device.return_value = None + mock_search_for_devices.side_effect = UsbError + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] is FlowResultType.ABORT + assert result1["reason"] == "usb_error" diff --git a/tests/components/energenie_power_sockets/test_init.py b/tests/components/energenie_power_sockets/test_init.py new file mode 100644 index 00000000000..4e2fe51665b --- /dev/null +++ b/tests/components/energenie_power_sockets/test_init.py @@ -0,0 +1,64 @@ +"""Tests for setting up Energenie-Power-Sockets integration.""" + +from unittest.mock import MagicMock + +from pyegps.exceptions import UsbError + +from homeassistant.components.energenie_power_sockets.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, +) -> None: + """Test loading and unloading the integration.""" + entry = valid_config_entry + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert DOMAIN not in hass.data + + +async def test_device_not_found_on_load_entry( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, +) -> None: + """Test device not available on config entry setup.""" + + mock_get_device.return_value = None + + valid_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(valid_config_entry.entry_id) + await hass.async_block_till_done() + + assert valid_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_usb_error( + hass: HomeAssistant, valid_config_entry: MockConfigEntry, mock_get_device: MagicMock +) -> None: + """Test no USB access on config entry setup.""" + + mock_get_device.side_effect = UsbError + + valid_config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(valid_config_entry.entry_id) + await hass.async_block_till_done() + + assert valid_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/energenie_power_sockets/test_switch.py b/tests/components/energenie_power_sockets/test_switch.py new file mode 100644 index 00000000000..4cd2bd60028 --- /dev/null +++ b/tests/components/energenie_power_sockets/test_switch.py @@ -0,0 +1,134 @@ +"""Test the switch functionality.""" + +from unittest.mock import MagicMock + +from pyegps.exceptions import EgpsException +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.energenie_power_sockets.const import DOMAIN +from homeassistant.components.homeassistant import ( + DOMAIN as HOME_ASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def _test_switch_on_off( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch on/off service.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, + ) + + assert hass.states.get(entity_id).state == STATE_OFF + + +async def _test_switch_on_exeception( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch on service with USBError side effect.""" + dev.switch_on.side_effect = EgpsException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + HOME_ASSISTANT_DOMAIN, + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) + dev.switch_on.side_effect = None + + +async def _test_switch_off_exeception( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch off service with USBError side effect.""" + dev.switch_off.side_effect = EgpsException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, + ) + dev.switch_off.side_effect = None + + +async def _test_switch_update_exception( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch update with USBError side effect.""" + dev.get_status.side_effect = EgpsException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_UPDATE_ENTITY, + {"entity_id": entity_id}, + blocking=True, + ) + dev.get_status.side_effect = None + + +@pytest.mark.parametrize( + "entity_name", + [ + "mockedusbdevice_socket_0", + "mockedusbdevice_socket_1", + "mockedusbdevice_socket_2", + "mockedusbdevice_socket_3", + ], +) +async def test_switch_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, + entity_name: str, + snapshot: SnapshotAssertion, +) -> None: + """Test setup and functionality of device switches.""" + + entry = valid_config_entry + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] + + state = hass.states.get(f"switch.{entity_name}") + assert state == snapshot + assert entity_registry.async_get(state.entity_id) == snapshot + + device_mock = mock_get_device.return_value + await _test_switch_on_off(hass, state.entity_id, device_mock) + await _test_switch_on_exeception(hass, state.entity_id, device_mock) + await _test_switch_off_exeception(hass, state.entity_id, device_mock) + await _test_switch_update_exception(hass, state.entity_id, device_mock) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/energyzero/test_config_flow.py b/tests/components/energyzero/test_config_flow.py index d16ea5cc8a8..a9fe8534fd5 100644 --- a/tests/components/energyzero/test_config_flow.py +++ b/tests/components/energyzero/test_config_flow.py @@ -20,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert "flow_id" in result @@ -29,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/enigma2/__init__.py b/tests/components/enigma2/__init__.py new file mode 100644 index 00000000000..15580d55b17 --- /dev/null +++ b/tests/components/enigma2/__init__.py @@ -0,0 +1 @@ +"""Tests for the Enigma2 integration.""" diff --git a/tests/components/enigma2/conftest.py b/tests/components/enigma2/conftest.py new file mode 100644 index 00000000000..9bbbda895bd --- /dev/null +++ b/tests/components/enigma2/conftest.py @@ -0,0 +1,90 @@ +"""Test the Enigma2 config flow.""" + +from homeassistant.components.enigma2.const import ( + CONF_DEEP_STANDBY, + CONF_MAC_ADDRESS, + CONF_SOURCE_BOUQUET, + CONF_USE_CHANNEL_ICON, + DEFAULT_DEEP_STANDBY, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +MAC_ADDRESS = "12:34:56:78:90:ab" + +TEST_REQUIRED = { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + CONF_SSL: DEFAULT_SSL, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, +} + +TEST_FULL = { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + CONF_SSL: DEFAULT_SSL, + CONF_USERNAME: "root", + CONF_PASSWORD: "password", + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, +} + +TEST_IMPORT_FULL = { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + CONF_SSL: DEFAULT_SSL, + CONF_USERNAME: "root", + CONF_PASSWORD: "password", + CONF_NAME: "My Player", + CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY, + CONF_SOURCE_BOUQUET: "Favourites", + CONF_MAC_ADDRESS: MAC_ADDRESS, + CONF_USE_CHANNEL_ICON: False, +} + +TEST_IMPORT_REQUIRED = {CONF_HOST: "1.1.1.1"} + +EXPECTED_OPTIONS = { + CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY, + CONF_SOURCE_BOUQUET: "Favourites", + CONF_USE_CHANNEL_ICON: False, +} + + +class MockDevice: + """A mock Enigma2 device.""" + + mac_address: str | None = "12:34:56:78:90:ab" + _base = "http://1.1.1.1" + + async def _call_api(self, url: str) -> dict: + if url.endswith("/api/about"): + return { + "info": { + "ifaces": [ + { + "mac": self.mac_address, + } + ] + } + } + + def get_version(self): + """Return the version.""" + return None + + async def get_about(self) -> dict: + """Get mock about endpoint.""" + return await self._call_api("/api/about") + + async def close(self): + """Mock close.""" diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py new file mode 100644 index 00000000000..dfca569276d --- /dev/null +++ b/tests/components/enigma2/test_config_flow.py @@ -0,0 +1,166 @@ +"""Test the Enigma2 config flow.""" + +from typing import Any +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError +from openwebif.error import InvalidAuthError +import pytest + +from homeassistant import config_entries +from homeassistant.components.enigma2.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.issue_registry import IssueRegistry + +from .conftest import ( + EXPECTED_OPTIONS, + TEST_FULL, + TEST_IMPORT_FULL, + TEST_IMPORT_REQUIRED, + TEST_REQUIRED, + MockDevice, +) + + +@pytest.fixture +async def user_flow(hass: HomeAssistant) -> str: + """Return a user-initiated flow after filling in host info.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + return result["flow_id"] + + +@pytest.mark.parametrize( + ("test_config"), + [(TEST_FULL), (TEST_REQUIRED)], +) +async def test_form_user( + hass: HomeAssistant, user_flow: str, test_config: dict[str, Any] +): + """Test a successful user initiated flow.""" + with ( + patch( + "openwebif.api.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ), + patch( + "homeassistant.components.enigma2.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure(user_flow, test_config) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == test_config[CONF_HOST] + assert result["data"] == test_config + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error_type"), + [ + (InvalidAuthError, "invalid_auth"), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_user_errors( + hass: HomeAssistant, user_flow, exception: Exception, error_type: str +) -> None: + """Test we handle errors.""" + with patch( + "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_FULL) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + assert result["errors"] == {"base": error_type} + + +@pytest.mark.parametrize( + ("test_config", "expected_data", "expected_options"), + [ + (TEST_IMPORT_FULL, TEST_FULL, EXPECTED_OPTIONS), + (TEST_IMPORT_REQUIRED, TEST_REQUIRED, {}), + ], +) +async def test_form_import( + hass: HomeAssistant, + test_config: dict[str, Any], + expected_data: dict[str, Any], + expected_options: dict[str, Any], + issue_registry: IssueRegistry, +) -> None: + """Test we get the form with import source.""" + with ( + patch( + "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ), + patch( + "homeassistant.components.enigma2.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=test_config, + ) + await hass.async_block_till_done() + + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" + ) + + assert issue + assert issue.issue_domain == DOMAIN + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == test_config[CONF_HOST] + assert result["data"] == expected_data + assert result["options"] == expected_options + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error_type"), + [ + (InvalidAuthError, "invalid_auth"), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_import_errors( + hass: HomeAssistant, + exception: Exception, + error_type: str, + issue_registry: IssueRegistry, +) -> None: + """Test we handle errors on import.""" + with patch( + "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_IMPORT_FULL, + ) + + issue = issue_registry.async_get_issue( + DOMAIN, f"deprecated_yaml_{DOMAIN}_import_issue_{error_type}" + ) + + assert issue + assert issue.issue_domain == DOMAIN + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == error_type diff --git a/tests/components/enigma2/test_init.py b/tests/components/enigma2/test_init.py new file mode 100644 index 00000000000..93a130eef54 --- /dev/null +++ b/tests/components/enigma2/test_init.py @@ -0,0 +1,38 @@ +"""Test the Enigma2 integration init.""" + +from unittest.mock import patch + +from homeassistant.components.enigma2.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .conftest import TEST_REQUIRED, MockDevice + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test successful unload of entry.""" + with ( + patch( + "homeassistant.components.enigma2.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ), + patch( + "homeassistant.components.enigma2.media_player.async_setup_entry", + return_value=True, + ), + ): + entry = MockConfigEntry(domain=DOMAIN, data=TEST_REQUIRED, title="name") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index 45a4e6e387f..96c0843906f 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -2,11 +2,12 @@ from unittest.mock import Mock, patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.enocean.config_flow import EnOceanFlowHandler from homeassistant.components.enocean.const import DOMAIN from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +27,7 @@ async def test_user_flow_cannot_create_multiple_instances(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -39,7 +40,7 @@ async def test_user_flow_with_detected_dongle(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "detect" devices = result["data_schema"].schema.get("device").container assert FAKE_DONGLE_PATH in devices @@ -53,7 +54,7 @@ async def test_user_flow_with_no_detected_dongle(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -66,7 +67,7 @@ async def test_detection_flow_with_valid_path(hass: HomeAssistant) -> None: DOMAIN, context={"source": "detect"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -85,7 +86,7 @@ async def test_detection_flow_with_custom_path(hass: HomeAssistant) -> None: data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -104,7 +105,7 @@ async def test_detection_flow_with_invalid_path(hass: HomeAssistant) -> None: data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "detect" assert CONF_DEVICE in result["errors"] @@ -118,7 +119,7 @@ async def test_manual_flow_with_valid_path(hass: HomeAssistant) -> None: DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -134,7 +135,7 @@ async def test_manual_flow_with_invalid_path(hass: HomeAssistant) -> None: DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert CONF_DEVICE in result["errors"] @@ -150,7 +151,7 @@ async def test_import_flow_with_valid_path(hass: HomeAssistant) -> None: data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == DATA_TO_IMPORT[CONF_DEVICE] @@ -168,5 +169,5 @@ async def test_import_flow_with_invalid_path(hass: HomeAssistant) -> None: data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_dongle_path" diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 40d409aea8e..965af3b40fc 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock, patch +import jwt from pyenphase import ( Envoy, EnvoyData, @@ -342,7 +343,7 @@ def mock_envoy_fixture( @pytest.fixture(name="setup_enphase_envoy") -async def setup_enphase_envoy_fixture(hass, config, mock_envoy): +async def setup_enphase_envoy_fixture(hass: HomeAssistant, config, mock_envoy): """Define a fixture to set up Enphase Envoy.""" with ( patch( @@ -368,7 +369,10 @@ def mock_authenticate(): @pytest.fixture(name="mock_auth") def mock_auth(serial_number): """Define a mocked EnvoyAuth fixture.""" - return EnvoyTokenAuth("127.0.0.1", token="abc", envoy_serial=serial_number) + token = jwt.encode( + payload={"name": "envoy", "exp": 1907837780}, key="secret", algorithm="HS256" + ) + return EnvoyTokenAuth("127.0.0.1", token=token, envoy_serial=serial_number) @pytest.fixture(name="mock_setup") diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 51cda1cc478..c2ab51a7dbd 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -475,7 +475,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_production_l1', @@ -486,9 +486,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -503,17 +500,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -527,7 +514,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', @@ -538,9 +525,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -555,17 +539,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -577,7 +551,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', @@ -588,9 +562,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -605,16 +576,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -628,7 +590,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', @@ -639,9 +601,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -656,17 +615,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -680,7 +629,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_production_l2', @@ -691,9 +640,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -708,17 +654,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -732,7 +668,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', @@ -743,9 +679,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -760,17 +693,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -782,7 +705,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', @@ -793,9 +716,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -810,16 +730,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -833,7 +744,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', @@ -844,9 +755,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -861,17 +769,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -885,7 +783,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_production_l3', @@ -896,9 +794,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -913,17 +808,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -937,7 +822,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', @@ -948,9 +833,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -965,17 +847,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -987,7 +859,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', @@ -998,9 +870,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1015,16 +884,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1038,7 +898,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', @@ -1049,9 +909,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1066,17 +923,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1090,7 +937,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', @@ -1101,9 +948,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -1118,17 +962,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1142,7 +976,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', @@ -1153,9 +987,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1170,17 +1001,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1192,7 +1013,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', @@ -1203,9 +1024,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1220,16 +1038,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1243,7 +1052,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', @@ -1254,9 +1063,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1271,17 +1077,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1295,7 +1091,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', @@ -1306,9 +1102,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -1323,17 +1116,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1347,7 +1130,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', @@ -1358,9 +1141,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1375,17 +1155,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1397,7 +1167,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', @@ -1408,9 +1178,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1425,16 +1192,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1448,7 +1206,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', @@ -1459,9 +1217,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1476,17 +1231,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1500,7 +1245,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', @@ -1511,9 +1256,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -1528,17 +1270,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1552,7 +1284,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', @@ -1563,9 +1295,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1580,17 +1309,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1602,7 +1321,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', @@ -1613,9 +1332,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1630,16 +1346,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1653,7 +1360,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', @@ -1664,9 +1371,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1681,17 +1385,7 @@ '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', - }), + 'state': None, }), dict({ 'entity': dict({ diff --git a/tests/components/enphase_envoy/snapshots/test_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_sensor.ambr index 4024c43c655..cec9d5141cd 100644 --- a/tests/components/enphase_envoy/snapshots/test_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_sensor.ambr @@ -319,7 +319,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_production_l1', @@ -331,9 +331,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -358,7 +355,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_today_l1', @@ -370,9 +367,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -395,7 +389,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l1', @@ -407,9 +401,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -434,7 +425,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l1', @@ -446,9 +437,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -473,7 +461,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_production_l2', @@ -485,9 +473,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -512,7 +497,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_today_l2', @@ -524,9 +509,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -549,7 +531,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l2', @@ -561,9 +543,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -588,7 +567,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l2', @@ -600,9 +579,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -627,7 +603,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_production_l3', @@ -639,9 +615,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -666,7 +639,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_today_l3', @@ -678,9 +651,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -703,7 +673,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l3', @@ -715,9 +685,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -742,7 +709,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l3', @@ -754,9 +721,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -781,7 +745,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_consumption_l1', @@ -793,9 +757,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -820,7 +781,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l1', @@ -832,9 +793,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -857,7 +815,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l1', @@ -869,9 +827,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -896,7 +851,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l1', @@ -908,9 +863,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -935,7 +887,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_consumption_l2', @@ -947,9 +899,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -974,7 +923,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l2', @@ -986,9 +935,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1011,7 +957,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l2', @@ -1023,9 +969,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1050,7 +993,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l2', @@ -1062,9 +1005,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1089,7 +1029,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_consumption_l3', @@ -1101,9 +1041,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1128,7 +1065,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l3', @@ -1140,9 +1077,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1165,7 +1099,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l3', @@ -1177,9 +1111,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1204,7 +1135,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l3', @@ -1216,9 +1147,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3487,55 +3415,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_current_power_consumption_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power consumption l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_consumption_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.324', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_consumption_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power consumption l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_consumption_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.324', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_consumption_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power consumption l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_consumption_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.324', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_production-state] StateSnapshot({ @@ -3555,55 +3441,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_current_power_production_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power production l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_production_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.234', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_production_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power production l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_production_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.234', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_production_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power production l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_production_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.234', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days-state] StateSnapshot({ @@ -3622,52 +3466,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption last seven days l1', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.321', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption last seven days l2', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.321', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption last seven days l3', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.321', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today-state] StateSnapshot({ @@ -3687,55 +3492,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption today l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.323', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption today l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.323', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption today l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.323', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days-state] StateSnapshot({ @@ -3754,52 +3517,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production last seven days l1', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.231', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production last seven days l2', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.231', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production last seven days l3', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.231', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_today-state] StateSnapshot({ @@ -3819,55 +3543,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_production_today_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production today l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_today_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.233', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_today_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production today l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_today_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.233', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_today_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production today l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_today_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.233', - }) + None # --- # name: test_sensor[sensor.envoy_1234_frequency_net_consumption_ct-state] None @@ -3951,55 +3633,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy consumption l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.001322', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy consumption l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.002322', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy consumption l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.003322', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production-state] StateSnapshot({ @@ -4019,55 +3659,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy production l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.001232', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy production l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.002232', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy production l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.003232', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_net_energy_consumption-state] StateSnapshot({ diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 9000cf92e0e..2709087a543 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Enphase Envoy config flow.""" from ipaddress import ip_address +import logging from unittest.mock import AsyncMock from pyenphase import EnvoyAuthenticationError, EnvoyError @@ -11,6 +12,11 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: @@ -18,7 +24,7 @@ async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -30,7 +36,7 @@ async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy 1234" assert result2["data"] == { "host": "1.1.1.1", @@ -48,7 +54,7 @@ async def test_user_no_serial_number( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -60,7 +66,7 @@ async def test_user_no_serial_number( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy" assert result2["data"] == { "host": "1.1.1.1", @@ -78,7 +84,7 @@ async def test_user_fetching_serial_fails( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -90,7 +96,7 @@ async def test_user_fetching_serial_fails( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy" assert result2["data"] == { "host": "1.1.1.1", @@ -120,7 +126,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, setup_enphase_envoy) -> No }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -142,7 +148,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, setup_enphase_envoy) -> }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -164,7 +170,7 @@ async def test_form_unknown_error(hass: HomeAssistant, setup_enphase_envoy) -> N }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -193,7 +199,7 @@ async def test_zeroconf_pre_token_firmware( type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert _get_schema_default(result["data_schema"].schema, "username") == "installer" @@ -207,7 +213,7 @@ async def test_zeroconf_pre_token_firmware( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy 1234" assert result2["result"].unique_id == "1234" assert result2["data"] == { @@ -235,7 +241,7 @@ async def test_zeroconf_token_firmware( type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert _get_schema_default(result["data_schema"].schema, "username") == "" @@ -248,7 +254,7 @@ async def test_zeroconf_token_firmware( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy 1234" assert result2["result"].unique_id == "1234" assert result2["data"] == { @@ -278,7 +284,7 @@ async def test_form_host_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # existing config @@ -295,7 +301,7 @@ async def test_form_host_already_exists( "password": "wrong-password", }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} # still original config after failure @@ -313,7 +319,7 @@ async def test_form_host_already_exists( }, ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" # updated config with new ip and changed pw @@ -323,9 +329,13 @@ async def test_form_host_already_exists( async def test_zeroconf_serial_already_exists( - hass: HomeAssistant, config_entry, setup_enphase_envoy + hass: HomeAssistant, + config_entry, + setup_enphase_envoy, + caplog: pytest.LogCaptureFixture, ) -> None: """Test serial number already exists from zeroconf.""" + _LOGGER.setLevel(logging.DEBUG) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -340,10 +350,11 @@ async def test_zeroconf_serial_already_exists( ), ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data["host"] == "4.4.4.4" + assert "Zeroconf ip 4 processing 4.4.4.4, current hosts: {'1.1.1.1'}" in caplog.text async def test_zeroconf_serial_already_exists_ignores_ipv6( @@ -364,7 +375,7 @@ async def test_zeroconf_serial_already_exists_ignores_ipv6( ), ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" assert config_entry.data["host"] == "1.1.1.1" @@ -389,13 +400,240 @@ async def test_zeroconf_host_already_exists( ), ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "1234" assert config_entry.title == "Envoy 1234" +async def test_zero_conf_while_form( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test zeroconf while form is active.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protovers": "7.0.1"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data["host"] == "1.1.1.1" + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + + +async def test_zero_conf_second_envoy_while_form( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test zeroconf while form is active.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("4.4.4.4"), + ip_addresses=[ip_address("4.4.4.4")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "4321", "protovers": "7.0.1"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result2["type"] is FlowResultType.FORM + assert config_entry.data["host"] == "1.1.1.1" + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "host": "4.4.4.4", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "Envoy 4321" + assert result3["result"].unique_id == "4321" + + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result4["type"] is FlowResultType.ABORT + + +async def test_zero_conf_malformed_serial_property( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test malformed zeroconf properties.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + with pytest.raises(KeyError) as ex: + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serilnum": "1234", "protovers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert "serialnum" in str(ex.value) + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result3["type"] is FlowResultType.ABORT + + +async def test_zero_conf_malformed_serial( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test malformed zeroconf properties.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "12%4", "protovers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result2["type"] is FlowResultType.FORM + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "Envoy 12%4" + + +async def test_zero_conf_malformed_fw_property( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test malformed zeroconf property.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protvers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data["host"] == "1.1.1.1" + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + + +async def test_zero_conf_old_blank_entry( + hass: HomeAssistant, setup_enphase_envoy +) -> None: + """Test re-using old blank entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "username": "", + "password": "", + "name": "unknown", + }, + unique_id=None, + title="Envoy", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1"), ip_address("1.1.1.2")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protovers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert entry.data["host"] == "1.1.1.1" + assert entry.unique_id == "1234" + assert entry.title == "Envoy 1234" + + async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> None: """Test we reauth auth.""" result = await hass.config_entries.flow.async_init( @@ -414,7 +652,7 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> }, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index e9513644947..3571c74cdcc 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -6,10 +6,11 @@ import xml.etree.ElementTree as et import aiohttp import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.environment_canada.const import CONF_STATION, DOMAIN from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -65,7 +66,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE @@ -93,7 +94,7 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None: flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +123,7 @@ async def test_exception_handling(hass: HomeAssistant, error) -> None: {}, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} @@ -142,6 +143,6 @@ async def test_lat_lon_not_specified(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=fake_config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE diff --git a/tests/components/epic_games_store/__init__.py b/tests/components/epic_games_store/__init__.py new file mode 100644 index 00000000000..1c5baf3704f --- /dev/null +++ b/tests/components/epic_games_store/__init__.py @@ -0,0 +1 @@ +"""Tests for the Epic Games Store integration.""" diff --git a/tests/components/epic_games_store/common.py b/tests/components/epic_games_store/common.py new file mode 100644 index 00000000000..95191ad97f9 --- /dev/null +++ b/tests/components/epic_games_store/common.py @@ -0,0 +1,31 @@ +"""Common methods used across tests for Epic Games Store.""" + +from unittest.mock import patch + +from homeassistant.components.epic_games_store.const import DOMAIN +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_COUNTRY, MOCK_LANGUAGE + +from tests.common import MockConfigEntry + + +async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry: + """Set up the Epic Games Store platform.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + unique_id=f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}", + ) + mock_entry.add_to_hass(hass) + + with patch("homeassistant.components.epic_games_store.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/epic_games_store/conftest.py b/tests/components/epic_games_store/conftest.py new file mode 100644 index 00000000000..e02997a429e --- /dev/null +++ b/tests/components/epic_games_store/conftest.py @@ -0,0 +1,44 @@ +"""Define fixtures for Epic Games Store tests.""" + +from unittest.mock import Mock, patch + +import pytest + +from .const import ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND, + DATA_FREE_GAMES, + DATA_FREE_GAMES_CHRISTMAS_SPECIAL, +) + + +@pytest.fixture(name="service_multiple") +def mock_service_multiple(): + """Mock a successful service with multiple free & discount games.""" + with patch( + "homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI" + ) as service_mock: + instance = service_mock.return_value + instance.get_free_games = Mock(return_value=DATA_FREE_GAMES) + yield service_mock + + +@pytest.fixture(name="service_christmas_special") +def mock_service_christmas_special(): + """Mock a successful service with Christmas special case.""" + with patch( + "homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI" + ) as service_mock: + instance = service_mock.return_value + instance.get_free_games = Mock(return_value=DATA_FREE_GAMES_CHRISTMAS_SPECIAL) + yield service_mock + + +@pytest.fixture(name="service_attribute_not_found") +def mock_service_attribute_not_found(): + """Mock a successful service returning a not found attribute error with free & discount games.""" + with patch( + "homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI" + ) as service_mock: + instance = service_mock.return_value + instance.get_free_games = Mock(return_value=DATA_ERROR_ATTRIBUTE_NOT_FOUND) + yield service_mock diff --git a/tests/components/epic_games_store/const.py b/tests/components/epic_games_store/const.py new file mode 100644 index 00000000000..dcd82c7e03e --- /dev/null +++ b/tests/components/epic_games_store/const.py @@ -0,0 +1,25 @@ +"""Test constants.""" + +from homeassistant.components.epic_games_store.const import DOMAIN + +from tests.common import load_json_object_fixture + +MOCK_LANGUAGE = "fr" +MOCK_COUNTRY = "FR" + +DATA_ERROR_ATTRIBUTE_NOT_FOUND = load_json_object_fixture( + "error_1004_attribute_not_found.json", DOMAIN +) + +DATA_ERROR_WRONG_COUNTRY = load_json_object_fixture( + "error_5222_wrong_country.json", DOMAIN +) + +# free games +DATA_FREE_GAMES = load_json_object_fixture("free_games.json", DOMAIN) + +DATA_FREE_GAMES_ONE = load_json_object_fixture("free_games_one.json", DOMAIN) + +DATA_FREE_GAMES_CHRISTMAS_SPECIAL = load_json_object_fixture( + "free_games_christmas_special.json", DOMAIN +) diff --git a/tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json b/tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json new file mode 100644 index 00000000000..6cb14608c2b --- /dev/null +++ b/tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json @@ -0,0 +1,1026 @@ +{ + "errors": [ + { + "message": "CatalogOffer/offerMappings: Request failed with status code 404", + "locations": [ + { + "line": 73, + "column": 17 + } + ], + "correlationId": "0451aa13-b1d6-4f90-8ca5-d12bf917675a", + "serviceResponse": "{\"errorMessage\":\"The item or resource being requested could not be found.\",\"errorCode\":\"errors.com.epicgames.not_found\",\"numericErrorCode\":1004,\"errorStatus\":404}", + "stack": null, + "path": ["Catalog", "searchStore", "elements", 4, "offerMappings"] + }, + { + "message": "CatalogNamespace/mappings: Request failed with status code 404", + "locations": [ + { + "line": 68, + "column": 19 + } + ], + "correlationId": "0451aa13-b1d6-4f90-8ca5-d12bf917675a", + "serviceResponse": "{\"errorMessage\":\"The item or resource being requested could not be found.\",\"errorCode\":\"errors.com.epicgames.not_found\",\"numericErrorCode\":1004,\"errorStatus\":404}", + "stack": null, + "path": ["Catalog", "searchStore", "elements", 4, "catalogNs", "mappings"] + } + ], + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Godlike Burger", + "id": "d9300ace164b41ac90a7b54e59d47953", + "namespace": "beb7e64d3da74ae780405da48cccb581", + "description": "Dans Godlike Burger, vous g\u00e9rez le restaurant le plus d\u00e9ment de la galaxie\u00a0! Assommez, empoisonnez et tuez les clients... pour les transformer en steaks\u00a0! Mais nulle crainte\u00a0: la client\u00e8le alien reviendra si vous la jouez fine, car c'est trop bon de s'adonner au cannibalisme.", + "effectiveDate": "2022-04-21T17:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2022-03-28T18:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/f42598038b9343e58d27e0a8c0b831b6/godlike-burger-offer-1trpc.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/f42598038b9343e58d27e0a8c0b831b6/download-godlike-burger-offer-8u2uh.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/f42598038b9343e58d27e0a8c0b831b6/download-godlike-burger-offer-8u2uh.jpg" + } + ], + "seller": { + "id": "o-d2ygr9bjcjfebgt8842wvvbmswympz", + "name": "Daedalic Entertainment" + }, + "productSlug": null, + "urlSlug": "37b001690e2a4d6f872567cdd06f0c6f", + "url": null, + "items": [ + { + "id": "c027f1bc9db54f189ad938634500e542", + "namespace": "beb7e64d3da74ae780405da48cccb581" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "false" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21894" + }, + { + "id": "19847" + }, + { + "id": "1083" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1263" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "godlike-burger-4150a0", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "godlike-burger-4150a0", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 1999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "19,99\u00a0\u20ac", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "1c2dc8194022428da305eedb42ed574d", + "endDate": "2023-10-12T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-05T15:00:00.000Z", + "endDate": "2023-10-12T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Destiny\u00a02\u00a0: Pack 30e anniversaire Bungie", + "id": "e7b9e222c7274dd28714aba2e06d2a01", + "namespace": "428115def4ca4deea9d69c99c5a5a99e", + "description": "Le Pack 30e anniversaire inclut un nouveau donjon, le lance-roquettes exotique Gjallarhorn, de nouvelles armes et armures, et plus encore. ", + "effectiveDate": "2022-08-23T13:00:00.000Z", + "offerType": "DLC", + "expiryDate": null, + "viewableDate": "2022-08-08T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S4_1200x1600_1200x1600-04ebd49752c682d003014680f3d5be18" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S3_2560x1440_2560x1440-b2f882323923927c414ab23faf1022ca" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_OfferLogo_200x200_200x200-234225abe0aca2bfa7f5c5bc6e6fe348" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S4_1200x1600_1200x1600-04ebd49752c682d003014680f3d5be18" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S3_2560x1440_2560x1440-b2f882323923927c414ab23faf1022ca" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot1_1920x1080-37c070caa0106b08910518150bf96e94" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot2_1920x1080-14490e3ec01dceedce23d870774b2393" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot3_1920x1080-fdf882ad2cc98be7e63516b4ad28d6e9" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot4_1920x1080-079d4e12a8a04b31f7d4def7f4b745e7" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot5_1920x1080-f3c958c685629b6678544cba8bffc483" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot6_1920x1080-f13bb310baf9c158d15d473474c11586" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot7_1920x1080-6d2b714d2cfd64623cdcc39487d0b429" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot8_1920x1080-0956ff1a3a4969d9a3f2b96d87bdc19d" + } + ], + "seller": { + "id": "o-49lqsefbl6zr5sy3ztak77ej97cuvh", + "name": "Bungie" + }, + "productSlug": null, + "urlSlug": "destiny-2--bungie-30th-anniversary-pack", + "url": null, + "items": [ + { + "id": "904b57fb8bcd41a6be6c690a92ab3c15", + "namespace": "428115def4ca4deea9d69c99c5a5a99e" + } + ], + "customAttributes": [], + "categories": [ + { + "path": "addons" + }, + { + "path": "freegames" + }, + { + "path": "addons/durable" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1203" + }, + { + "id": "1210" + }, + { + "id": "1370" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "destiny-2", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "destiny-2--bungie-30th-anniversary-pack", + "pageType": "addon--cms-hybrid" + } + ], + "price": { + "totalPrice": { + "discountPrice": 2499, + "originalPrice": 2499, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "24,99\u00a0\u20ac", + "discountPrice": "24,99\u00a0\u20ac", + "intermediatePrice": "24,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-11T16:00:00.000Z", + "endDate": "2023-10-25T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 60 + } + } + ] + } + ] + } + }, + { + "title": "Gloomhaven", + "id": "9232fdbc352445cc820a54bdc97ed2bb", + "namespace": "bc079f73f020432fac896d30c8e2c330", + "description": "Que vous soyez arriv\u00e9s \u00e0 Gloomhaven en r\u00e9pondant \u00e0 l'appel de l'aventure ou au d\u00e9sir cupide de l'\u00e9clat de l'or, votre destin n'en sera pas chang\u00e9...", + "effectiveDate": "2022-09-22T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2022-09-22T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/ef2777467a3c49059a076e42fd9b41f0/gloomhaven-offer-1j9mc.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/ef2777467a3c49059a076e42fd9b41f0/download-gloomhaven-offer-1ho2x.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/ef2777467a3c49059a076e42fd9b41f0/download-gloomhaven-offer-1ho2x.jpg" + } + ], + "seller": { + "id": "o-4x4bpaww55p5g3f6xpyqe2cneqxd5d", + "name": "Asmodee" + }, + "productSlug": null, + "urlSlug": "0d48da287df14493a7415b560ec1bbb3", + "url": null, + "items": [ + { + "id": "6047532dd78a456593d0ffd6602a7218", + "namespace": "bc079f73f020432fac896d30c8e2c330" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetViewableDate", + "value": "true" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + }, + { + "key": "isBlockchainUsed", + "value": "false" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "29088" + }, + { + "id": "21122" + }, + { + "id": "1188" + }, + { + "id": "21127" + }, + { + "id": "19847" + }, + { + "id": "21129" + }, + { + "id": "1386" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1264" + }, + { + "id": "21137" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "16979" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1367" + }, + { + "id": "22776" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "21147" + }, + { + "id": "21149" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "gloomhaven-92f741", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "gloomhaven-92f741", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 3499, + "originalPrice": 3499, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "34,99\u00a0\u20ac", + "discountPrice": "34,99\u00a0\u20ac", + "intermediatePrice": "34,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "911 Operator", + "id": "268fd6ea355740d6ba4c76c3ffd4cbe0", + "namespace": "d923c737f0d243ccab407605ea40d39e", + "description": "911 OPERATOR est un jeu o\u00f9 tu deviens op\u00e9rateur de la ligne des urgences et o\u00f9 tu r\u00e9sous des incidents en fournissant des instruction et en g\u00e9rant des \u00e9quipes de secours. Tu peux jouer sur la carte de n\u2019importe quelle ville* du monde!", + "effectiveDate": "2023-09-14T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2023-09-07T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/c06cc46c27954f55974e9e7a4f3b3849/911-operator-omkv7.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/c06cc46c27954f55974e9e7a4f3b3849/911-operator-8dcp7.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/c06cc46c27954f55974e9e7a4f3b3849/911-operator-8dcp7.jpg" + } + ], + "seller": { + "id": "o-8dv8wz77w8tqnymmm8e99p28eny7kg", + "name": "Games Operators S.A." + }, + "productSlug": null, + "urlSlug": "ecb09cc5f55345e6bf6d3d9354c12876", + "url": null, + "items": [ + { + "id": "07499df5530b45c3ad8464a96cbe26c7", + "namespace": "d923c737f0d243ccab407605ea40d39e" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetViewableDate", + "value": "true" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + }, + { + "key": "isBlockchainUsed", + "value": "false" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + } + ], + "tags": [ + { + "id": "1393" + }, + { + "id": "19847" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "1263" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "911-operator-585edd", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "911-operator-585edd", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1349, + "originalPrice": 1349, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "13,49\u00a0\u20ac", + "discountPrice": "13,49\u00a0\u20ac", + "intermediatePrice": "13,49\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-23T14:00:00.000Z", + "endDate": "2023-10-30T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 50 + } + } + ] + } + ] + } + }, + { + "title": "Q.U.B.E. ULTIMATE BUNDLE", + "id": "f18f14a76a874aa883a651fcc8c513d0", + "namespace": "0712c5eca64b47bbbced82cabba9f0d7", + "description": "Q.U.B.E. ULTIMATE BUNDLE", + "effectiveDate": "2023-10-12T15:00:00.000Z", + "offerType": "BUNDLE", + "expiryDate": null, + "viewableDate": "2023-10-05T14:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Portrait_V2_1200x1600-981ac683de50fd5afed2c87dbc26494a" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Landscape_V2_2560x1440-50dbecaa32e134e246717f8a5e60ad25" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Logo_V2_400x400-99dcb7d141728efbe2b1b4e993ce6339" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Portrait_V2_1200x1600-981ac683de50fd5afed2c87dbc26494a" + } + ], + "seller": { + "id": "o-kk34ewvmscclj5a2ukx49ff6qknn7a", + "name": "Ten Hut Games" + }, + "productSlug": "qube-ultimate-bundle", + "urlSlug": "qube-ultimate-bundle", + "url": null, + "items": [ + { + "id": "11d229f51ac1445a8925b8d14da82b9b", + "namespace": "ad43401ad02840c2b2bee5f1f1a59988" + }, + { + "id": "0e7ec1d579ab481c93dff6056c19299f", + "namespace": "4b5f1eb366dc45f0920d397c01b291ba" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.productSlug", + "value": "qube-ultimate-bundle" + } + ], + "categories": [ + { + "path": "bundles" + }, + { + "path": "freegames" + }, + { + "path": "bundles/games" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "1298" + }, + { + "id": "1203" + }, + { + "id": "1117" + }, + { + "id": "1294" + } + ], + "catalogNs": { + "mappings": null + }, + "offerMappings": null, + "price": { + "totalPrice": { + "discountPrice": 4499, + "originalPrice": 4499, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "44,99\u00a0\u20ac", + "discountPrice": "44,99\u00a0\u20ac", + "intermediatePrice": "44,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-12T15:00:00.000Z", + "endDate": "2023-10-19T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + }, + { + "title": "PAYDAY 2", + "id": "de434b7be57940d98ede93b50cdacfc2", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "PAYDAY 2 is an action-packed, four-player co-op shooter that once again lets gamers don the masks of the original PAYDAY crew - Dallas, Hoxton, Wolf and Chains - as they descend on Washington DC for an epic crime spree.", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "viewableDate": "2023-06-01T14:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/mammoth-h1nvv_2560x1440-ac346d6ece5ec356561e112fbddb2dc1" + }, + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/EN-mega-sale-vault-16x9-asset_1920x1080-a27cf3919dde320a72936374a1d47813" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "payday-2-c66369", + "urlSlug": "mystery-game-7", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/mega-sale" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "payday-2-c66369" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Blazing Sails", + "id": "363d0be3b57d4741a046d38da0e6355e", + "namespace": "aee7dd76aa6746578f476dc47f8d1d7f", + "description": "Survivez \u00e0 Blazing Sails, un jeu de pirate en JcJ tr\u00e9pidant\u00a0! Cr\u00e9ez votre navire et vos pirates uniques. Naviguez en \u00e9quipe avec d'autres joueurs\u00a0! D\u00e9couvrez diff\u00e9rents modes de jeu, cartes, armes, types de navires et bien plus encore. Battez les \u00e9quipages adverses dans d'\u00e9piques combats sur terre et en mer\u00a0!", + "effectiveDate": "2099-04-06T17:35:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2023-03-30T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_S2_1200x1600-bae3831e97b560958dc785e830ebed8c" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_S1_2560x1440-fd7a7b3d357555880cb7969634553c5b" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_IC1_400x400-a7b91f257fcbd9ced825d3da95298170" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_S2_1200x1600-bae3831e97b560958dc785e830ebed8c" + } + ], + "seller": { + "id": "o-ftmts7pjfvdywkby885rdzl4hdbtys", + "name": "Iceberg Interactive" + }, + "productSlug": "blazing-sails", + "urlSlug": "blazing-sails", + "url": null, + "items": [ + { + "id": "30aec28f450a41499dd27e0d27294b56", + "namespace": "aee7dd76aa6746578f476dc47f8d1d7f" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "KR" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "blazing-sails" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "1264" + }, + { + "id": "1203" + }, + { + "id": "9547" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "blazing-sails", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1479, + "originalPrice": 1479, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "14,79\u00a0\u20ac", + "discountPrice": "14,79\u00a0\u20ac", + "intermediatePrice": "14,79\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-12T15:00:00.000Z", + "endDate": "2023-10-19T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + } + ], + "paging": { + "count": 1000, + "total": 7 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/error_5222_wrong_country.json b/tests/components/epic_games_store/fixtures/error_5222_wrong_country.json new file mode 100644 index 00000000000..c91d5551ff9 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/error_5222_wrong_country.json @@ -0,0 +1,23 @@ +{ + "errors": [ + { + "message": "CatalogQuery/searchStore: Request failed with status code 400", + "locations": [ + { + "line": 18, + "column": 9 + } + ], + "correlationId": "e10ad58e-a4f9-4097-af5d-cafdbe0d8bbd", + "serviceResponse": "{\"errorCode\":\"errors.com.epicgames.catalog.invalid_country_code\",\"errorMessage\":\"Sorry the value you entered: en-US, does not appear to be a valid ISO country code.\",\"messageVars\":[\"en-US\"],\"numericErrorCode\":5222,\"originatingService\":\"com.epicgames.catalog.public\",\"intent\":\"prod\",\"errorStatus\":400}", + "stack": null, + "path": ["Catalog", "searchStore"] + } + ], + "data": { + "Catalog": { + "searchStore": null + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/free_games.json b/tests/components/epic_games_store/fixtures/free_games.json new file mode 100644 index 00000000000..29ff43f32a0 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/free_games.json @@ -0,0 +1,2189 @@ +{ + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Rising Storm 2: Vietnam", + "id": "b19d810d322240e7b37bcf84ffac60ce", + "namespace": "3542a1df211e492bb2abecb7c734f7f9", + "description": "Red Orchestra Series' take on Vietnam: 64-player MP matches; 20+ maps; US Army & Marines, PAVN/NVA, NLF/VC; Australians and ARVN forces; 50+ weapons; 4 flyable helicopters; mines, traps and tunnels; Brutal. Authentic. Gritty. Character customization.", + "effectiveDate": "2020-10-08T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S3-2560x1440-e08edd93cb71bf15b50a74f3de2d17b0.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S3-2560x1440-e08edd93cb71bf15b50a74f3de2d17b0.jpg" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + } + ], + "seller": { + "id": "o-2baznhy8tfh7fmyb55ul656v7ggt7r", + "name": "Tripwire Interactive" + }, + "productSlug": "rising-storm-2-vietnam/home", + "urlSlug": "risingstorm2vietnam", + "url": null, + "items": [ + { + "id": "685765c3f37049c49b45bea4173725d2", + "namespace": "3542a1df211e492bb2abecb7c734f7f9" + }, + { + "id": "c7c6d65ac4cc4ef0ae12e8e89f134684", + "namespace": "3542a1df211e492bb2abecb7c734f7f9" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "publisherName", + "value": "Tripwire Interactive" + }, + { + "key": "developerName", + "value": "Antimatter Games" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "rising-storm-2-vietnam/home" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21122" + }, + { + "id": "21125" + }, + { + "id": "21129" + }, + { + "id": "14346" + }, + { + "id": "9547" + }, + { + "id": "16011" + }, + { + "id": "15375" + }, + { + "id": "21135" + }, + { + "id": "21138" + }, + { + "id": "1299" + }, + { + "id": "16979" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "17493" + }, + { + "id": "21141" + }, + { + "id": "22485" + }, + { + "id": "18777" + }, + { + "id": "18778" + }, + { + "id": "1115" + }, + { + "id": "21148" + }, + { + "id": "21149" + }, + { + "id": "14944" + }, + { + "id": "19242" + }, + { + "id": "18607" + }, + { + "id": "1203" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "rising-storm-2-vietnam", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 2199, + "originalPrice": 2199, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac21.99", + "discountPrice": "\u20ac21.99", + "intermediatePrice": "\u20ac21.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-11-03T15:00:00.000Z", + "endDate": "2022-11-10T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + }, + { + "title": "Idle Champions of the Forgotten Realms", + "id": "a9748abde1c94b66aae5250bb9fc5503", + "namespace": "7e508f543b05465abe3a935960eb70ac", + "description": "Idle Champions is a licensed Dungeons & Dragons strategy management video game uniting iconic characters from novels, campaigns, and shows into one epic adventure.", + "effectiveDate": "2021-02-16T17:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S2_1200x1600-dd9a8f25ad56089231f43cf639bde217" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S1_2560x1440-e2a1ffd224f443594d5deff3a47a45e2" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S2_1200x1600-dd9a8f25ad56089231f43cf639bde217" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S2_1200x1600-dd9a8f25ad56089231f43cf639bde217" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S1_2560x1440-e2a1ffd224f443594d5deff3a47a45e2" + } + ], + "seller": { + "id": "o-3kpjwtwqwfl2p9wdwvpad7yqz4kt6c", + "name": "Codename Entertainment" + }, + "productSlug": "idle-champions-of-the-forgotten-realms", + "urlSlug": "banegeneralaudience", + "url": null, + "items": [ + { + "id": "9a4e1a1eb6b140f6a9e5e4dcb5a2bf55", + "namespace": "7e508f543b05465abe3a935960eb70ac" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "KR" + }, + { + "key": "publisherName", + "value": "Codename Entertainment" + }, + { + "key": "developerName", + "value": "Codename Entertainment" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "idle-champions-of-the-forgotten-realms" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "21136" + }, + { + "id": "21122" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "1188" + }, + { + "id": "1141" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "21149" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "idle-champions-of-the-forgotten-realms", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Hundred Days - Winemaking Simulator", + "id": "141eee80fbe041d48e16e7b998829295", + "namespace": "4d8b727a49144090b103f6b6ba471e71", + "description": "Winemaking could be your best adventure. Make the best wine interacting with soil and nature and take your winery to the top. Your beautiful journey into the winemaking tradition starts now.", + "effectiveDate": "2021-05-13T14:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_G1C_00-1920x1080-0ffeb0645f0badb615627b481b4a913e.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S1-2560x1440-8f0dd95b6027cd1243361d430b3bf552.jpg" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + } + ], + "seller": { + "id": "o-ty5rvlnsbgdnfffytsywat86gcedkm", + "name": "Broken Arms Games srls" + }, + "productSlug": "hundred-days-winemaking-simulator", + "urlSlug": "hundred-days-winemaking-simulator", + "url": null, + "items": [ + { + "id": "03cacb8754f243bfbc536c9dda0eb32e", + "namespace": "4d8b727a49144090b103f6b6ba471e71" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "developerName", + "value": "Broken Arms Games" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "hundred-days-winemaking-simulator" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1188" + }, + { + "id": "21894" + }, + { + "id": "21127" + }, + { + "id": "19242" + }, + { + "id": "21130" + }, + { + "id": "16011" + }, + { + "id": "9547" + }, + { + "id": "1263" + }, + { + "id": "15375" + }, + { + "id": "18607" + }, + { + "id": "1393" + }, + { + "id": "21138" + }, + { + "id": "16979" + }, + { + "id": "21140" + }, + { + "id": "17493" + }, + { + "id": "21141" + }, + { + "id": "18777" + }, + { + "id": "1370" + }, + { + "id": "18778" + }, + { + "id": "21146" + }, + { + "id": "1115" + }, + { + "id": "21149" + }, + { + "id": "10719" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "hundred-days-winemaking-simulator", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1999, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac19.99", + "intermediatePrice": "\u20ac19.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Shadow of the Tomb Raider: Definitive Edition", + "id": "ee7f3c6725fd4fd4b8aeab8622cb770e", + "namespace": "4b5461ca8d1c488787b5200b420de066", + "description": "In Shadow of the Tomb Raider Definitive Edition experience the final chapter of Lara\u2019s origin as she is forged into the Tomb Raider she is destined to be.", + "effectiveDate": "2021-12-30T16:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s1-2560x1440-eca6506e95a1_2560x1440-193582a5fd76a593804e0171d6395cf4" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s1-2560x1440-eca6506e95a1_2560x1440-193582a5fd76a593804e0171d6395cf4" + } + ], + "seller": { + "id": "o-7petn7mrlk8g86ktqm7uglcr7lfaja", + "name": "Square Enix" + }, + "productSlug": "shadow-of-the-tomb-raider", + "urlSlug": "shadow-of-the-tomb-raider", + "url": null, + "items": [ + { + "id": "e7f90759e0544e42be9391d10a5c6000", + "namespace": "4b5461ca8d1c488787b5200b420de066" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "shadow-of-the-tomb-raider" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21122" + }, + { + "id": "18051" + }, + { + "id": "1188" + }, + { + "id": "21894" + }, + { + "id": "21127" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21109" + }, + { + "id": "21141" + }, + { + "id": "22485" + }, + { + "id": "1370" + }, + { + "id": "21146" + }, + { + "id": "1117" + }, + { + "id": "21149" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "shadow-of-the-tomb-raider", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1319, + "originalPrice": 3999, + "voucherDiscount": 0, + "discount": 2680, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac39.99", + "discountPrice": "\u20ac13.19", + "intermediatePrice": "\u20ac13.19" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "35111a3c715340d08910a9f6a5b3e846", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-18T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 33 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Terraforming Mars", + "id": "f2496286331e405793d69807755b7b23", + "namespace": "25d726130e6c4fe68f88e71933bda955", + "description": "The taming of the Red Planet has begun!\n\nControl your corporation, play project cards, build up production, place your cities and green areas on the map, and race for milestones and awards!\n\nWill your corporation lead the way into humanity's new era?", + "effectiveDate": "2022-05-05T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/5199b206e46947ebad5e5c282e95776f/terraforming-mars-offer-1j70f.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/5199b206e46947ebad5e5c282e95776f/download-terraforming-mars-offer-13t2e.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/5199b206e46947ebad5e5c282e95776f/download-terraforming-mars-offer-13t2e.jpg" + } + ], + "seller": { + "id": "o-4x4bpaww55p5g3f6xpyqe2cneqxd5d", + "name": "Asmodee" + }, + "productSlug": null, + "urlSlug": "24cdfcde68bf4a7e8b8618ac2c0c460b", + "url": null, + "items": [ + { + "id": "ee49486d7346465dba1f1dec85725aee", + "namespace": "25d726130e6c4fe68f88e71933bda955" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "18051" + }, + { + "id": "1188" + }, + { + "id": "21125" + }, + { + "id": "1386" + }, + { + "id": "9547" + }, + { + "id": "21138" + }, + { + "id": "1203" + }, + { + "id": "1299" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "21148" + }, + { + "id": "21149" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "terraforming-mars-18c3ad", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "terraforming-mars-18c3ad", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1399, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 600, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac13.99", + "intermediatePrice": "\u20ac13.99" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "8e9732952e714f6583416e66fc451cd7", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-18T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 70 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Car Mechanic Simulator 2018", + "id": "5eb27cf1747c40b5a0d4f5492774678d", + "namespace": "226306adde104c9092247dcd4bfa1499", + "description": "Build and expand your repair service empire in this incredibly detailed and highly realistic simulation game, where attention to car detail is astonishing. Find classic, unique cars in the new Barn Find module and Junkyard module.", + "effectiveDate": "2022-06-23T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/226306adde104c9092247dcd4bfa1499/EGS_CarMechanicSimulator2018_RedDotGames_S2_1200x1600-f285924f9144353f57ac4631f0c689e6" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/226306adde104c9092247dcd4bfa1499/EGS_CarMechanicSimulator2018_RedDotGames_S1_2560x1440-3489ef1499e64c168fdf4b14926d2c23" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/226306adde104c9092247dcd4bfa1499/EGS_CarMechanicSimulator2018_RedDotGames_S2_1200x1600-f285924f9144353f57ac4631f0c689e6" + } + ], + "seller": { + "id": "o-5n5cbrasl5yzexjc529rypg8eh8lfb", + "name": "PlayWay" + }, + "productSlug": "car-mechanic-simulator-2018", + "urlSlug": "car-mechanic-simulator-2018", + "url": null, + "items": [ + { + "id": "49a3a8597c4240ecaf1f9068106c9869", + "namespace": "226306adde104c9092247dcd4bfa1499" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "car-mechanic-simulator-2018" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "21120" + }, + { + "id": "1188" + }, + { + "id": "21127" + }, + { + "id": "9547" + }, + { + "id": "1393" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1370" + }, + { + "id": "21146" + }, + { + "id": "21148" + }, + { + "id": "21149" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "car-mechanic-simulator-2018", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1599, + "originalPrice": 1599, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac15.99", + "discountPrice": "\u20ac15.99", + "intermediatePrice": "\u20ac15.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "A Game Of Thrones: The Board Game Digital Edition", + "id": "a125d72a47a1490aba78c4e79a40395d", + "namespace": "1b737464d3c441f8956315433be02d3b", + "description": "It is the digital adaptation of the top-selling strategy board game from Fantasy Flight Games.", + "effectiveDate": "2022-06-23T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/61c1413e3db0423f9ddd4a5edbee717e/a-game-of-thrones-offer-11gxu.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/61c1413e3db0423f9ddd4a5edbee717e/download-a-game-of-thrones-offer-1q8ei.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/61c1413e3db0423f9ddd4a5edbee717e/download-a-game-of-thrones-offer-1q8ei.jpg" + } + ], + "seller": { + "id": "o-4x4bpaww55p5g3f6xpyqe2cneqxd5d", + "name": "Asmodee" + }, + "productSlug": null, + "urlSlug": "ce6f7ab4edab4cc2aa7e0ff4c19540e2", + "url": null, + "items": [ + { + "id": "dc6ae31efba7401fa72ed93f0bd37c6a", + "namespace": "1b737464d3c441f8956315433be02d3b" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "18051" + }, + { + "id": "1188" + }, + { + "id": "21125" + }, + { + "id": "9547" + }, + { + "id": "21138" + }, + { + "id": "1203" + }, + { + "id": "1299" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "21149" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "a-game-of-thrones-5858a3", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "a-game-of-thrones-5858a3", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1399, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 600, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac13.99", + "intermediatePrice": "\u20ac13.99" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "689de276cf3245a7bffdfa0d20500150", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-18T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 70 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Filament", + "id": "296453e71c884f95aecf4d582cf66915", + "namespace": "89fb09a222a54e53b692e9c36e68d0a1", + "description": "Solve challenging cable-based puzzles and uncover what really happened to the crew of The Alabaster. Now with Hint System (for those ultra tricky puzzles).", + "effectiveDate": "2022-08-11T11:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/5a72e62648d747189d2f5e7abb47444c/filament-offer-qrwye.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/5a72e62648d747189d2f5e7abb47444c/download-filament-offer-mk58q.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/5a72e62648d747189d2f5e7abb47444c/download-filament-offer-mk58q.jpg" + } + ], + "seller": { + "id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx", + "name": "Kasedo Games Ltd" + }, + "productSlug": null, + "urlSlug": "323de464947e4ee5a035c525b6b78021", + "url": null, + "items": [ + { + "id": "d4fa1325ef014725a89cc40e9b99e43d", + "namespace": "89fb09a222a54e53b692e9c36e68d0a1" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "1298" + }, + { + "id": "21894" + }, + { + "id": "19847" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1263" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "filament-332a92", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "filament-332a92", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1699, + "originalPrice": 1699, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac16.99", + "discountPrice": "\u20ac16.99", + "intermediatePrice": "\u20ac16.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-11-03T15:00:00.000Z", + "endDate": "2022-11-10T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + }, + { + "title": "Warhammer 40,000: Mechanicus - Standard Edition", + "id": "559b16fa81134dce83b5b8b7cf67b5b3", + "namespace": "144f9e231e2846d1a4381d9bb678f69d", + "description": "Take control of the most technologically advanced army in the Imperium - The Adeptus Mechanicus. Your every decision will weigh heavily on the outcome of the mission, in this turn-based tactical game. Will you be blessed by the Omnissiah?", + "effectiveDate": "2022-08-11T11:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/d26f2f9ea65c462dbd39040ae8389d36/warhammer-mechanicus-offer-17fnz.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/d26f2f9ea65c462dbd39040ae8389d36/download-warhammer-mechanicus-offer-1f6bv.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/d26f2f9ea65c462dbd39040ae8389d36/download-warhammer-mechanicus-offer-1f6bv.jpg" + } + ], + "seller": { + "id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx", + "name": "Kasedo Games Ltd" + }, + "productSlug": null, + "urlSlug": "f37159d9bd96489ab1b99bdad1ee796c", + "url": null, + "items": [ + { + "id": "f923ad9f3428472ab67baa4618c205a0", + "namespace": "144f9e231e2846d1a4381d9bb678f69d" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "21894" + }, + { + "id": "19847" + }, + { + "id": "1386" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "9549" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "warhammer-mechanicus-0e4b71", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "warhammer-mechanicus-0e4b71", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 2999, + "voucherDiscount": 0, + "discount": 2999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac29.99", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "7a3ee39632f5458990b6a9ad295881b8", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-27T15:00:00.000Z", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Fallout 3: Game of the Year Edition", + "id": "d6f01b1827c64ed388191ae507fe7c1b", + "namespace": "fa702d34a37248ba98fb17f680c085e3", + "description": "Prepare for the Future\u2122\nExperience the most acclaimed game of 2008 like never before with Fallout 3: Game of the Year Edition. Create a character of your choosing and descend into a post-apocalyptic world where every minute is a fight for survival", + "effectiveDate": "2022-10-20T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_S2_1200x1600-e2ba392652a1f57c4feb65d6bbd1f963" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_S1_2560x1440-073f5b4cf358f437a052a3c29806efa0" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_IC1_400x400-5e37dfe1d35c9ccf25c8889fe7218613" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_S2_1200x1600-e2ba392652a1f57c4feb65d6bbd1f963" + } + ], + "seller": { + "id": "o-bthbhn6wd7fzj73v5p4436ucn3k37u", + "name": "Bethesda Softworks LLC" + }, + "productSlug": "fallout-3-game-of-the-year-edition", + "urlSlug": "fallout-3-game-of-the-year-edition", + "url": null, + "items": [ + { + "id": "6b750e631e414927bde5b3e13b647443", + "namespace": "fa702d34a37248ba98fb17f680c085e3" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "fallout-3-game-of-the-year-edition" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "21122" + }, + { + "id": "1188" + }, + { + "id": "21894" + }, + { + "id": "21127" + }, + { + "id": "9547" + }, + { + "id": "21137" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1367" + }, + { + "id": "1370" + }, + { + "id": "1307" + }, + { + "id": "21147" + }, + { + "id": "21148" + }, + { + "id": "1117" + }, + { + "id": "21149" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "fallout-3-game-of-the-year-edition", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 659, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 1340, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac6.59", + "intermediatePrice": "\u20ac6.59" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "779554ee7a604b0091a4335a60b6e55a", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-27T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 33 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Evoland Legendary Edition", + "id": "e068e168886a4a90a4e36a310e3bda32", + "namespace": "3f7bd21610f743e598fa8e955500f5b7", + "description": "Evoland Legendary Edition brings you two great and unique RPGs, with their graphic style and gameplay changing as you progress through the game!", + "effectiveDate": "2022-10-20T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/aafde465b31e4bd5a169ff1c8a164a17/evoland-legendary-edition-1y7m0.png" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/aafde465b31e4bd5a169ff1c8a164a17/evoland-legendary-edition-1j93v.png" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/aafde465b31e4bd5a169ff1c8a164a17/evoland-legendary-edition-1j93v.png" + } + ], + "seller": { + "id": "o-ealhln64lfep9ww929uq9qcdmbyfn4", + "name": "Shiro Games SAS" + }, + "productSlug": null, + "urlSlug": "224c60bb93864e1c8a1900bcf7d661dd", + "url": null, + "items": [ + { + "id": "c829f27d0ab0406db8edf2b97562ee93", + "namespace": "3f7bd21610f743e598fa8e955500f5b7" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition" + }, + { + "path": "games" + }, + { + "path": "games/edition/base" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21109" + }, + { + "id": "1367" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "1117" + }, + { + "id": "9549" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "evoland-legendary-edition-5753ec", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "evoland-legendary-edition-5753ec", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1999, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac19.99", + "intermediatePrice": "\u20ac19.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Saturnalia", + "id": "275d5915ebd2479f983f51025b22a1b8", + "namespace": "c749cd78da34408d8434a46271f4bb79", + "description": "A Survival Horror Adventure: as an ensemble cast, explore an isolated village of ancient ritual \u2013 its labyrinthine roads change each time you lose all your characters.", + "effectiveDate": "2022-10-27T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S3_2560x1440-3cd916a7260b77c8488f8f2b0f3a51ab" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S3_2560x1440-3cd916a7260b77c8488f8f2b0f3a51ab" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + } + ], + "seller": { + "id": "o-cjwnkas5rn476tzk72fbh2ftutnc2y", + "name": "Santa Ragione" + }, + "productSlug": "saturnalia", + "urlSlug": "saturnalia", + "url": null, + "items": [ + { + "id": "dbce8ecb6923490c9404529651251216", + "namespace": "c749cd78da34408d8434a46271f4bb79" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.productSlug", + "value": "saturnalia" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1218" + }, + { + "id": "19847" + }, + { + "id": "1080" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "1117" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "saturnalia", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 1999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "8fa8f62eac9e4cab9fe242987c0f0988", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-27T15:00:00.000Z", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Maneater", + "id": "a22a7af179c54b86a93f3193ace8f7f4", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Maneater", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-tease-generic-promo-1920x1080_1920x1080-f7742c265e217510835ed14e04c48b4b" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-tease-generic-promo-1920x1080_1920x1080-f7742c265e217510835ed14e04c48b4b" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-carousel-mobile-thumbnail-1200x1600_1200x1600-1f45bf1ceb21c1ca2947f6df5ece5346" + }, + { + "type": "VaultOpened", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w4-1920x1080_1920x1080-2df36fe63c18ff6fcb5febf3dd7ed06e" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w4-1920x1080_1920x1080-2df36fe63c18ff6fcb5febf3dd7ed06e" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w4-1920x1080_1920x1080-2df36fe63c18ff6fcb5febf3dd7ed06e" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "maneater", + "urlSlug": "game-4", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "free-games" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "maneater" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Wolfenstein: The New Order", + "id": "1d41b93230e54bdd80c559d72adb7f4f", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Wolfenstein: The New Order", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-tease-generic-promo-1920x1080_1920x1080-f7742c265e217510835ed14e04c48b4b" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-carousel-mobile-thumbnail-1200x1600_1200x1600-1f45bf1ceb21c1ca2947f6df5ece5346" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w3-1920x1080_1920x1080-4a501d33fb4ac641e3e1e290dcc0e6c1" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w3-1920x1080_1920x1080-4a501d33fb4ac641e3e1e290dcc0e6c1" + }, + { + "type": "VaultOpened", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w3-1920x1080_1920x1080-4a501d33fb4ac641e3e1e290dcc0e6c1" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "wolfenstein-the-new-order", + "urlSlug": "game-3", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "free-games" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "wolfenstein-the-new-order" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + } + ], + "paging": { + "count": 1000, + "total": 14 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/free_games_christmas_special.json b/tests/components/epic_games_store/fixtures/free_games_christmas_special.json new file mode 100644 index 00000000000..0c65f47d3a0 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/free_games_christmas_special.json @@ -0,0 +1,253 @@ +{ + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Cursed to Golf", + "id": "0e4551e4ae65492b88009f8a4e41d778", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Cursed to Golf", + "effectiveDate": "2023-12-27T16:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": "2023-12-28T16:00:00.000Z", + "viewableDate": "2023-12-26T15:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-9_1920x1080-418a8fa10dd305bb2a219a7ec869c5ef" + }, + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-9-teaser_1920x1080-e71ae0041736db5ac259a355cb301116" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "cursed-to-golf-a6bc22", + "urlSlug": "mysterygame-9", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/holiday-sale" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "cursed-to-golf-a6bc22" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-12-27T16:00:00.000Z", + "endDate": "2023-12-28T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + }, + { + "startDate": "2023-12-27T16:00:00.000Z", + "endDate": "2023-12-28T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Mystery Game Day 10", + "id": "a8c3537a579943a688e3bd355ae36209", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Mystery Game Day 10", + "effectiveDate": "2099-01-01T16:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "viewableDate": "2023-12-27T15:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-10-teaser_1920x1080-3ea48042a44263bf1a0a59c725b6d95b" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-10-teaser_1920x1080-3ea48042a44263bf1a0a59c725b6d95b" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "[]", + "urlSlug": "mysterygame-10", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/holiday-sale" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "[]" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-12-28T16:00:00.000Z", + "endDate": "2023-12-29T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + } + ], + "paging": { + "count": 1000, + "total": 2 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/free_games_one.json b/tests/components/epic_games_store/fixtures/free_games_one.json new file mode 100644 index 00000000000..48cd64f68d4 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/free_games_one.json @@ -0,0 +1,658 @@ +{ + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Borderlands 3 Season Pass", + "id": "c3913a91e07b43cfbbbcfd8244c86dcc", + "namespace": "catnip", + "description": "Prolongez votre aventure dans Borderlands\u00a03 avec le Season Pass, regroupant des \u00e9l\u00e9ments cosm\u00e9tiques exclusifs et quatre histoires additionnelles, pour encore plus de missions et de d\u00e9fis\u00a0!", + "effectiveDate": "2019-09-11T12:00:00.000Z", + "offerType": "DLC", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/catnip/Diesel_productv2_borderlands-3_season-pass_BL3_SEASONPASS_Hero-3840x2160-4411e63a005a43811a2bc516ae7ec584598fd4aa-3840x2160-b8988ebb0f3d9159671e8968af991f30_3840x2160-b8988ebb0f3d9159671e8968af991f30" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075" + } + ], + "seller": { + "id": "o-37m6jbj5wcvrcvm4wusv7nazdfvbjk", + "name": "2K Games, Inc." + }, + "productSlug": "borderlands-3/season-pass", + "urlSlug": "borderlands-3--season-pass", + "url": null, + "items": [ + { + "id": "e9fdc1a9f47b4a5e8e63841c15de2b12", + "namespace": "catnip" + }, + { + "id": "fbc46bb6056940d2847ee1e80037a9af", + "namespace": "catnip" + }, + { + "id": "ff8e1152ddf742b68f9ac0cecd378917", + "namespace": "catnip" + }, + { + "id": "939e660825764e208938ab4f26b4da56", + "namespace": "catnip" + }, + { + "id": "4c43a9a691114ccd91c1884ab18f4e27", + "namespace": "catnip" + }, + { + "id": "3a6a3f9b351b4b599808df3267669b83", + "namespace": "catnip" + }, + { + "id": "ab030a9f53f3428fb2baf2ddbb0bb5ac", + "namespace": "catnip" + }, + { + "id": "ff96eef22b0e4c498e8ed80ac0030325", + "namespace": "catnip" + }, + { + "id": "5021e93a73374d6db1c1ce6c92234f8f", + "namespace": "catnip" + }, + { + "id": "9c0b1eb3265340678dff0fcb106402b1", + "namespace": "catnip" + }, + { + "id": "8c826db6e14f44aeac8816e1bd593632", + "namespace": "catnip" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "SA" + }, + { + "key": "publisherName", + "value": "2K" + }, + { + "key": "developerName", + "value": "Gearbox Software" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "borderlands-3/season-pass" + } + ], + "categories": [ + { + "path": "addons" + }, + { + "path": "freegames" + }, + { + "path": "addons/durable" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1264" + }, + { + "id": "16004" + }, + { + "id": "14869" + }, + { + "id": "26789" + }, + { + "id": "1367" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1294" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "borderlands-3", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "borderlands-3--season-pass", + "pageType": "addon--cms-hybrid" + } + ], + "price": { + "totalPrice": { + "discountPrice": 4999, + "originalPrice": 4999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "49,99\u00a0\u20ac", + "discountPrice": "49,99\u00a0\u20ac", + "intermediatePrice": "49,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 30 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 30 + } + } + ] + } + ] + } + }, + { + "title": "Call of the Sea", + "id": "92da5d8d918543b6b408e36d9af81765", + "namespace": "5e427319eea1401ab20c6cd78a4163c4", + "description": "Call of the Sea is an otherworldly tale of mystery and love set in the 1930s South Pacific. Explore a lush island paradise, solve puzzles and unlock secrets in the hunt for your husband\u2019s missing expedition.", + "effectiveDate": "2022-02-17T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S1_2560x1440-204699c6410deef9c18be0ee392f8335" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S5_1920x1080-7b22dfebdd9fcdde6e526c5dc4c16eb1" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + } + ], + "seller": { + "id": "o-fay4ghw9hhamujs53rfhy83ffexb7k", + "name": "Raw Fury" + }, + "productSlug": "call-of-the-sea", + "urlSlug": "call-of-the-sea", + "url": null, + "items": [ + { + "id": "cbc9c76c4bfc4bc6b28abb3afbcbf07a", + "namespace": "5e427319eea1401ab20c6cd78a4163c4" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.productSlug", + "value": "call-of-the-sea" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1296" + }, + { + "id": "1298" + }, + { + "id": "21894" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "1117" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "call-of-the-sea", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1999, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "19,99\u00a0\u20ac", + "discountPrice": "19,99\u00a0\u20ac", + "intermediatePrice": "19,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 60 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 60 + } + } + ] + } + ] + } + }, + { + "title": "Rise of Industry", + "id": "c04a2ab8ff4442cba0a41fb83453e701", + "namespace": "9f101e25b1a9427a9e6971d2b21c5f82", + "description": "Mettez vos comp\u00e9tences entrepreneuriales \u00e0 l'\u00e9preuve en cr\u00e9ant et en optimisant des cha\u00eenes de production complexes tout en gardant un \u0153il sur les r\u00e9sultats financiers. \u00c0 l'aube du 20e si\u00e8cle, appr\u00eatez-vous \u00e0 entrer dans un \u00e2ge d'or industriel, ou une d\u00e9pression historique.", + "effectiveDate": "2022-08-11T11:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/rise-of-industry-offer-1p22f.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/download-rise-of-industry-offer-1uujr.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/download-rise-of-industry-offer-1uujr.jpg" + } + ], + "seller": { + "id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx", + "name": "Kasedo Games Ltd" + }, + "productSlug": null, + "urlSlug": "f88fedc022fe488caaedaa5c782ff90d", + "url": null, + "items": [ + { + "id": "9f5b48a778824e6aa330d2c1a47f41b2", + "namespace": "9f101e25b1a9427a9e6971d2b21c5f82" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "26789" + }, + { + "id": "19847" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "rise-of-industry-0af838", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "rise-of-industry-0af838", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 2999, + "voucherDiscount": 0, + "discount": 2999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "29,99\u00a0\u20ac", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "a19d30dc34f44923993e68b82b75a084", + "endDate": "2023-03-09T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-02T16:00:00.000Z", + "endDate": "2023-03-09T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + } + ] + } + ] + } + }, + { + "title": "Dishonored - Definitive Edition", + "id": "4d25d74b88d1474a8ab21ffb88ca6d37", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Experience the definitive Dishonored collection. This complete compilation includes Dishonored as well as all of its additional content - Dunwall City Trials, The Knife of Dunwall, The Brigmore Witches and Void Walker\u2019s Arsenal.", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/15days-day15-wrapped-desktop-carousel-image_1920x1080-ebecfa7c79f02a9de5bca79560bee953" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/15days-day15-Unwrapped-desktop-carousel-image1_1920x1080-1992edb42bb8554ddeb14d430ba3f858" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/DAY15-carousel-mobile-unwrapped-image1_1200x1600-9716d77667d2a82931c55a4e4130989e" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "dishonored-definitive-edition", + "urlSlug": "mystery-game15", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/holiday-sale" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "dishonored-definitive-edition" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + } + ], + "paging": { + "count": 1000, + "total": 4 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/test_calendar.py b/tests/components/epic_games_store/test_calendar.py new file mode 100644 index 00000000000..46ca974f85c --- /dev/null +++ b/tests/components/epic_games_store/test_calendar.py @@ -0,0 +1,162 @@ +"""Tests for the Epic Games Store calendars.""" + +from unittest.mock import Mock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.calendar import ( + DOMAIN as CALENDAR_DOMAIN, + EVENT_END_DATETIME, + EVENT_START_DATETIME, + SERVICE_GET_EVENTS, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .common import setup_platform + +from tests.common import async_fire_time_changed + + +async def test_setup_component(hass: HomeAssistant, service_multiple: Mock) -> None: + """Test setup component.""" + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.name == "Epic Games Store Discount games" + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.name == "Epic Games Store Free games" + + +async def test_discount_games( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_multiple: Mock, +) -> None: + """Test discount games calendar.""" + freezer.move_to("2022-10-15T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.state == STATE_OFF + + freezer.move_to("2022-10-30T00:00:00.000Z") + async_fire_time_changed(hass) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.state == STATE_ON + + cal_attrs = dict(state.attributes) + assert cal_attrs == { + "friendly_name": "Epic Games Store Discount games", + "message": "Shadow of the Tomb Raider: Definitive Edition", + "all_day": False, + "start_time": "2022-10-18 08:00:00", + "end_time": "2022-11-01 08:00:00", + "location": "", + "description": "In Shadow of the Tomb Raider Definitive Edition experience the final chapter of Lara\u2019s origin as she is forged into the Tomb Raider she is destined to be.\n\nhttps://store.epicgames.com/fr/p/shadow-of-the-tomb-raider", + } + + +async def test_free_games( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_multiple: Mock, +) -> None: + """Test free games calendar.""" + freezer.move_to("2022-10-30T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.state == STATE_ON + + cal_attrs = dict(state.attributes) + assert cal_attrs == { + "friendly_name": "Epic Games Store Free games", + "message": "Warhammer 40,000: Mechanicus - Standard Edition", + "all_day": False, + "start_time": "2022-10-27 08:00:00", + "end_time": "2022-11-03 08:00:00", + "location": "", + "description": "Take control of the most technologically advanced army in the Imperium - The Adeptus Mechanicus. Your every decision will weigh heavily on the outcome of the mission, in this turn-based tactical game. Will you be blessed by the Omnissiah?\n\nhttps://store.epicgames.com/fr/p/warhammer-mechanicus-0e4b71", + } + + +async def test_attribute_not_found( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_attribute_not_found: Mock, +) -> None: + """Test setup calendars with attribute not found error.""" + freezer.move_to("2023-10-12T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.name == "Epic Games Store Discount games" + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.name == "Epic Games Store Free games" + assert state.state == STATE_ON + + +async def test_christmas_special( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_christmas_special: Mock, +) -> None: + """Test setup calendars with Christmas special case.""" + freezer.move_to("2023-12-28T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.name == "Epic Games Store Discount games" + assert state.state == STATE_OFF + + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.name == "Epic Games Store Free games" + assert state.state == STATE_ON + + +async def test_get_events( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_multiple: Mock, +) -> None: + """Test setup component with calendars.""" + freezer.move_to("2022-10-30T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + # 1 week in range of data + result = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + ATTR_ENTITY_ID: ["calendar.epic_games_store_discount_games"], + EVENT_START_DATETIME: dt_util.parse_datetime("2022-10-20T00:00:00.000Z"), + EVENT_END_DATETIME: dt_util.parse_datetime("2022-10-27T00:00:00.000Z"), + }, + blocking=True, + return_response=True, + ) + + assert len(result["calendar.epic_games_store_discount_games"]["events"]) == 3 + + # 1 week out of range of data + result = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + ATTR_ENTITY_ID: ["calendar.epic_games_store_discount_games"], + EVENT_START_DATETIME: dt_util.parse_datetime("1970-01-01T00:00:00.000Z"), + EVENT_END_DATETIME: dt_util.parse_datetime("1970-01-08T00:00:00.000Z"), + }, + blocking=True, + return_response=True, + ) + + assert len(result["calendar.epic_games_store_discount_games"]["events"]) == 0 diff --git a/tests/components/epic_games_store/test_config_flow.py b/tests/components/epic_games_store/test_config_flow.py new file mode 100644 index 00000000000..83e9cf9e99e --- /dev/null +++ b/tests/components/epic_games_store/test_config_flow.py @@ -0,0 +1,142 @@ +"""Test the Epic Games Store config flow.""" + +from http.client import HTTPException +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.epic_games_store.config_flow import get_default_language +from homeassistant.components.epic_games_store.const import DOMAIN +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND, + DATA_ERROR_WRONG_COUNTRY, + DATA_FREE_GAMES, + MOCK_COUNTRY, + MOCK_LANGUAGE, +) + + +async def test_default_language(hass: HomeAssistant) -> None: + """Test we get the form.""" + hass.config.language = "fr" + hass.config.country = "FR" + assert get_default_language(hass) == "fr" + + hass.config.language = "es" + hass.config.country = "ES" + assert get_default_language(hass) == "es-ES" + + hass.config.language = "en" + hass.config.country = "AZ" + assert get_default_language(hass) is None + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + return_value=DATA_FREE_GAMES, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["result"].unique_id == f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}" + assert ( + result2["title"] + == f"Epic Games Store - Free Games ({MOCK_LANGUAGE}-{MOCK_COUNTRY})" + ) + assert result2["data"] == { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + } + + +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.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + side_effect=HTTPException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect_wrong_param(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.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + return_value=DATA_ERROR_WRONG_COUNTRY, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_service_error(hass: HomeAssistant) -> None: + """Test we handle service error gracefully.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + return_value=DATA_ERROR_ATTRIBUTE_NOT_FOUND, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["result"].unique_id == f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}" + assert ( + result2["title"] + == f"Epic Games Store - Free Games ({MOCK_LANGUAGE}-{MOCK_COUNTRY})" + ) + assert result2["data"] == { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + } diff --git a/tests/components/epic_games_store/test_helper.py b/tests/components/epic_games_store/test_helper.py new file mode 100644 index 00000000000..155ccb7d211 --- /dev/null +++ b/tests/components/epic_games_store/test_helper.py @@ -0,0 +1,74 @@ +"""Tests for the Epic Games Store helpers.""" + +from typing import Any + +import pytest + +from homeassistant.components.epic_games_store.helper import ( + format_game_data, + get_game_url, + is_free_game, +) + +from .const import DATA_ERROR_ATTRIBUTE_NOT_FOUND, DATA_FREE_GAMES_ONE + +FREE_GAMES_API = DATA_FREE_GAMES_ONE["data"]["Catalog"]["searchStore"]["elements"] +FREE_GAME = FREE_GAMES_API[2] +NOT_FREE_GAME = FREE_GAMES_API[0] + + +def test_format_game_data() -> None: + """Test game data format.""" + game_data = format_game_data(FREE_GAME, "fr") + assert game_data + assert game_data["title"] + assert game_data["description"] + assert game_data["released_at"] + assert game_data["original_price"] + assert game_data["publisher"] + assert game_data["url"] + assert game_data["img_portrait"] + assert game_data["img_landscape"] + assert game_data["discount_type"] == "free" + assert game_data["discount_start_at"] + assert game_data["discount_end_at"] + + +@pytest.mark.parametrize( + ("raw_game_data", "expected_result"), + [ + ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][ + "elements" + ][1], + "/p/destiny-2--bungie-30th-anniversary-pack", + ), + ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][ + "elements" + ][4], + "/bundles/qube-ultimate-bundle", + ), + ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][ + "elements" + ][5], + "/p/mystery-game-7", + ), + ], +) +def test_get_game_url(raw_game_data: dict[str, Any], expected_result: bool) -> None: + """Test to get the game URL.""" + assert get_game_url(raw_game_data, "fr").endswith(expected_result) + + +@pytest.mark.parametrize( + ("raw_game_data", "expected_result"), + [ + (FREE_GAME, True), + (NOT_FREE_GAME, False), + ], +) +def test_is_free_game(raw_game_data: dict[str, Any], expected_result: bool) -> None: + """Test if this game is free.""" + assert is_free_game(raw_game_data) == expected_result diff --git a/tests/components/epion/test_config_flow.py b/tests/components/epion/test_config_flow.py index 8d246ac4dd4..c5d556996b6 100644 --- a/tests/components/epion/test_config_flow.py +++ b/tests/components/epion/test_config_flow.py @@ -33,7 +33,7 @@ async def test_user_flow(hass: HomeAssistant, mock_epion: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Epion integration" assert result["data"] == { CONF_API_KEY: API_KEY, @@ -63,7 +63,7 @@ async def test_form_exceptions( {CONF_API_KEY: API_KEY}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} mock_epion.return_value.get_current.side_effect = None @@ -78,7 +78,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Epion integration" assert result["data"] == { CONF_API_KEY: API_KEY, @@ -107,5 +107,5 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_epion: MagicMock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index c6ca921df0f..d485a4bfdef 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.epson.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-epson" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -61,7 +62,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -80,5 +81,5 @@ async def test_form_powered_off(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "powered_off"} diff --git a/tests/components/eq3btsmart/__init__.py b/tests/components/eq3btsmart/__init__.py new file mode 100644 index 00000000000..2d5fa84a9b8 --- /dev/null +++ b/tests/components/eq3btsmart/__init__.py @@ -0,0 +1 @@ +"""Tests for the eq3btsmart component.""" diff --git a/tests/components/eq3btsmart/conftest.py b/tests/components/eq3btsmart/conftest.py new file mode 100644 index 00000000000..19e10d6b59c --- /dev/null +++ b/tests/components/eq3btsmart/conftest.py @@ -0,0 +1,41 @@ +"""Fixtures for eq3btsmart tests.""" + +from bleak.backends.scanner import AdvertisementData +import pytest + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +from .const import MAC + +from tests.components.bluetooth import generate_ble_device + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +@pytest.fixture +def fake_service_info(): + """Return a BluetoothServiceInfoBleak for use in testing.""" + return BluetoothServiceInfoBleak( + name="CC-RT-BLE", + address=MAC, + rssi=0, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + connectable=False, + time=0, + device=generate_ble_device(address=MAC, name="CC-RT-BLE", rssi=0), + advertisement=AdvertisementData( + local_name="CC-RT-BLE", + manufacturer_data={}, + service_data={}, + service_uuids=[], + rssi=0, + tx_power=-127, + platform_data=(), + ), + ) diff --git a/tests/components/eq3btsmart/const.py b/tests/components/eq3btsmart/const.py new file mode 100644 index 00000000000..71b6564965c --- /dev/null +++ b/tests/components/eq3btsmart/const.py @@ -0,0 +1,4 @@ +"""Constants for the eq3btsmart tests.""" + +MAC = "aa:bb:cc:dd:ee:ff" +RSSI = -60 diff --git a/tests/components/eq3btsmart/test_config_flow.py b/tests/components/eq3btsmart/test_config_flow.py new file mode 100644 index 00000000000..f9db434850a --- /dev/null +++ b/tests/components/eq3btsmart/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the eq3btsmart config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.components.eq3btsmart.const import DOMAIN +from homeassistant.const import CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.device_registry import format_mac +from homeassistant.util import slugify + +from .const import MAC + +from tests.common import MockConfigEntry + + +async def test_user_flow(hass: HomeAssistant) -> None: + """Test we can handle a regular successflow setup flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_MAC: MAC}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == slugify(MAC) + assert result["data"] == {} + assert result["context"]["unique_id"] == MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_flow_invalid_mac(hass: HomeAssistant) -> None: + """Test we handle invalid mac address.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_MAC: "invalid"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {CONF_MAC: "invalid_mac_address"} + assert len(mock_setup_entry.mock_calls) == 0 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_MAC: MAC}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == slugify(MAC) + assert result["data"] == {} + assert result["context"]["unique_id"] == MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_flow( + hass: HomeAssistant, fake_service_info: BluetoothServiceInfoBleak +) -> None: + """Test we can handle a bluetooth discovery flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=fake_service_info, + ) + + with patch( + "homeassistant.components.eq3btsmart.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() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == slugify(MAC) + assert result["data"] == {} + assert result["context"]["unique_id"] == MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry(hass: HomeAssistant) -> None: + """Test duplicate setup handling.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_MAC: MAC, + }, + unique_id=format_mac(MAC), + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_MAC: MAC, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_setup_entry.call_count == 0 diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py index 7d467fc50a0..aa863bd4371 100644 --- a/tests/components/escea/test_config_flow.py +++ b/tests/components/escea/test_config_flow.py @@ -25,8 +25,7 @@ def mock_discovery_service_fixture() -> AsyncMock: @pytest.fixture(name="mock_controller") def mock_controller_fixture() -> MagicMock: """Mock controller.""" - controller = MagicMock() - return controller + return MagicMock() def _mock_start_discovery( @@ -60,12 +59,12 @@ async def test_not_found( ) # Confirmation form - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" assert discovery_service.return_value.close.call_count == 1 @@ -95,12 +94,12 @@ async def test_found( ) # Confirmation form - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_setup.call_count == 1 @@ -117,6 +116,6 @@ async def test_single_instance_allowed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" assert discovery_service.call_count == 0 diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index e51fc663b59..f71b4196be6 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -18,6 +18,7 @@ from aioesphomeapi import ( HomeassistantServiceCall, ReconnectLogic, UserService, + VoiceAssistantFeature, ) import pytest from zeroconf import Zeroconf @@ -180,7 +181,9 @@ async def mock_dashboard(hass): class MockESPHomeDevice: """Mock an esphome device.""" - def __init__(self, entry: MockConfigEntry, client: APIClient) -> None: + def __init__( + self, entry: MockConfigEntry, client: APIClient, device_info: DeviceInfo + ) -> None: """Init the mock.""" self.entry = entry self.client = client @@ -188,9 +191,11 @@ class MockESPHomeDevice: self.service_call_callback: Callable[[HomeassistantServiceCall], None] self.on_disconnect: Callable[[bool], None] self.on_connect: Callable[[bool], None] + self.on_connect_error: Callable[[Exception], None] self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] + self.device_info = device_info def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: """Set the state callback.""" @@ -222,10 +227,20 @@ class MockESPHomeDevice: """Set the connect callback.""" self.on_connect = on_connect + def set_on_connect_error( + self, on_connect_error: Callable[[Exception], None] + ) -> None: + """Set the connect error callback.""" + self.on_connect_error = on_connect_error + async def mock_connect(self) -> None: """Mock connecting.""" await self.on_connect() + async def mock_connect_error(self, exc: Exception) -> None: + """Mock connect error.""" + await self.on_connect_error(exc) + def set_home_assistant_state_subscription_callback( self, on_state_sub: Callable[[str, str | None], None], @@ -262,8 +277,6 @@ async def _mock_generic_device_entry( ) entry.add_to_hass(hass) - mock_device = MockESPHomeDevice(entry, mock_client) - default_device_info = { "name": "test", "friendly_name": "Test", @@ -272,6 +285,8 @@ async def _mock_generic_device_entry( } device_info = DeviceInfo(**(default_device_info | mock_device_info)) + mock_device = MockESPHomeDevice(entry, mock_client, device_info) + def _subscribe_states(callback: Callable[[EntityState], None]) -> None: """Subscribe to state.""" mock_device.set_state_callback(callback) @@ -290,7 +305,7 @@ async def _mock_generic_device_entry( """Subscribe to home assistant states.""" mock_device.set_home_assistant_state_subscription_callback(on_state_sub) - mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.device_info = AsyncMock(return_value=mock_device.device_info) mock_client.subscribe_voice_assistant = Mock() mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services @@ -309,6 +324,7 @@ async def _mock_generic_device_entry( super().__init__(*args, **kwargs) mock_device.set_on_disconnect(kwargs["on_disconnect"]) mock_device.set_on_connect(kwargs["on_connect"]) + mock_device.set_on_connect_error(kwargs["on_connect_error"]) self._try_connect = self.mock_try_connect async def mock_try_connect(self): @@ -342,10 +358,16 @@ async def mock_voice_assistant_entry( ): """Set up an ESPHome entry with voice assistant.""" - async def _mock_voice_assistant_entry(version: int) -> MockConfigEntry: + async def _mock_voice_assistant_entry( + voice_assistant_feature_flags: VoiceAssistantFeature, + ) -> MockConfigEntry: return ( await _mock_generic_device_entry( - hass, mock_client, {"voice_assistant_version": version}, ([], []), [] + hass, + mock_client, + {"voice_assistant_feature_flags": voice_assistant_feature_flags}, + ([], []), + [], ) ).entry @@ -355,13 +377,28 @@ async def mock_voice_assistant_entry( @pytest.fixture async def mock_voice_assistant_v1_entry(mock_voice_assistant_entry) -> MockConfigEntry: """Set up an ESPHome entry with voice assistant.""" - return await mock_voice_assistant_entry(version=1) + return await mock_voice_assistant_entry( + voice_assistant_feature_flags=VoiceAssistantFeature.VOICE_ASSISTANT + ) @pytest.fixture async def mock_voice_assistant_v2_entry(mock_voice_assistant_entry) -> MockConfigEntry: """Set up an ESPHome entry with voice assistant.""" - return await mock_voice_assistant_entry(version=2) + return await mock_voice_assistant_entry( + voice_assistant_feature_flags=VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.SPEAKER + ) + + +@pytest.fixture +async def mock_voice_assistant_api_entry(mock_voice_assistant_entry) -> MockConfigEntry: + """Set up an ESPHome entry with voice assistant.""" + return await mock_voice_assistant_entry( + voice_assistant_feature_flags=VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.SPEAKER + | VoiceAssistantFeature.API_AUDIO + ) @pytest.fixture diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index e06b96356ae..439092d9fb1 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -16,7 +16,7 @@ from aioesphomeapi import ( import aiohttp import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import DomainData, dashboard from homeassistant.components.esphome.const import ( @@ -56,7 +56,7 @@ async def test_user_connection_works( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -65,7 +65,7 @@ async def test_user_connection_works( data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 80, @@ -104,7 +104,7 @@ async def test_user_connection_updates_host( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -112,7 +112,7 @@ async def test_user_connection_updates_host( context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.1" @@ -136,14 +136,14 @@ async def test_user_sets_unique_id( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["type"] is FlowResultType.FORM assert discovery_result["step_id"] == "discovery_confirm" discovery_result = await hass.config_entries.flow.async_configure( discovery_result["flow_id"], {}, ) - assert discovery_result["type"] == FlowResultType.CREATE_ENTRY + assert discovery_result["type"] is FlowResultType.CREATE_ENTRY assert discovery_result["data"] == { CONF_HOST: "192.168.43.183", CONF_PORT: 6053, @@ -158,14 +158,14 @@ async def test_user_sets_unique_id( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_user_resolve_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} @@ -213,7 +213,7 @@ async def test_user_causes_zeroconf_to_abort( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["type"] is FlowResultType.FORM assert discovery_result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -222,14 +222,14 @@ async def test_user_causes_zeroconf_to_abort( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -253,7 +253,7 @@ async def test_user_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -274,14 +274,14 @@ async def test_user_with_password( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -304,7 +304,7 @@ async def test_user_invalid_password( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = InvalidAuthAPIError @@ -313,7 +313,7 @@ async def test_user_invalid_password( result["flow_id"], user_input={CONF_PASSWORD: "invalid"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_auth"} @@ -347,14 +347,14 @@ async def test_user_dashboard_has_wrong_key( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -402,7 +402,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -455,14 +455,14 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -510,14 +510,14 @@ async def test_user_discovers_name_and_dashboard_is_unavailable( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -540,7 +540,7 @@ async def test_login_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = APIConnectionError @@ -549,7 +549,7 @@ async def test_login_connection_error( result["flow_id"], user_input={CONF_PASSWORD: "valid"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "connection_error"} @@ -577,7 +577,7 @@ async def test_discovery_initiation( flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -602,7 +602,7 @@ async def test_discovery_no_mac( flow = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.ABORT + assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "mdns_missing_mac" @@ -631,7 +631,7 @@ async def test_discovery_already_configured( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -652,13 +652,13 @@ async def test_discovery_duplicate_data( result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -687,7 +687,7 @@ async def test_discovery_updates_unique_id( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "11:22:33:44:55:aa" @@ -705,7 +705,7 @@ async def test_user_requires_psk( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {} @@ -727,7 +727,7 @@ async def test_encryption_key_valid_psk( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info = AsyncMock( @@ -737,7 +737,7 @@ async def test_encryption_key_valid_psk( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -761,7 +761,7 @@ async def test_encryption_key_invalid_psk( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError @@ -769,7 +769,7 @@ async def test_encryption_key_invalid_psk( result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {"base": "invalid_psk"} assert mock_client.noise_psk == INVALID_NOISE_PSK @@ -793,7 +793,7 @@ async def test_reauth_initiation( "unique_id": entry.unique_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -821,7 +821,7 @@ async def test_reauth_confirm_valid( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -870,7 +870,7 @@ async def test_reauth_fixed_via_dashboard( }, ) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -913,7 +913,7 @@ async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( }, ) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK assert mock_config_entry.data[CONF_PASSWORD] == "" @@ -940,7 +940,7 @@ async def test_reauth_fixed_via_remove_password( }, ) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_PASSWORD] == "" @@ -976,7 +976,7 @@ async def test_reauth_fixed_via_dashboard_at_confirm( }, ) - assert result["type"] == FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result assert result["step_id"] == "reauth_confirm" mock_dashboard["configured"].append( @@ -995,7 +995,7 @@ async def test_reauth_fixed_via_dashboard_at_confirm( # We just fetch the form result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -1026,7 +1026,7 @@ async def test_reauth_confirm_invalid( result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -1038,7 +1038,7 @@ async def test_reauth_confirm_invalid( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -1068,7 +1068,7 @@ async def test_reauth_confirm_invalid_with_unique_id( result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -1080,7 +1080,7 @@ async def test_reauth_confirm_invalid_with_unique_id( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -1105,7 +1105,7 @@ async def test_discovery_dhcp_updates_host( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.184" @@ -1135,7 +1135,7 @@ async def test_discovery_dhcp_no_changes( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.183" @@ -1157,7 +1157,7 @@ async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "service_received" dash = dashboard.async_get_dashboard(hass) @@ -1188,7 +1188,7 @@ async def test_zeroconf_encryption_key_via_dashboard( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.FORM + assert flow["type"] is FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" mock_dashboard["configured"].append( @@ -1219,7 +1219,7 @@ async def test_zeroconf_encryption_key_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -1255,7 +1255,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.FORM + assert flow["type"] is FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" mock_dashboard["configured"].append( @@ -1285,7 +1285,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( assert len(mock_get_encryption_key.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -1320,7 +1320,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.FORM + assert flow["type"] is FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" await dashboard.async_get_dashboard(hass).async_refresh() @@ -1331,7 +1331,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard( flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" @@ -1352,7 +1352,7 @@ async def test_option_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS @@ -1369,7 +1369,7 @@ async def test_option_flow( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value} assert len(mock_reload.mock_calls) == int(option_value) @@ -1398,14 +1398,14 @@ async def test_user_discovers_name_no_dashboard( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 51b9b535caa..01c1553cf42 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -60,7 +60,7 @@ async def test_restore_dashboard_storage_end_to_end( ) as mock_dashboard_api: await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_dashboard_api.mock_calls[0][1][0] == "http://new-host:6052" @@ -74,7 +74,7 @@ async def test_setup_dashboard_fails( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_get_devices.call_count == 1 # The dashboard addon might recover later so we still @@ -109,7 +109,7 @@ async def test_setup_dashboard_fails_when_already_setup( await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_get_devices.call_count == 1 # We still setup, and reload, but we do not do the reauths assert dashboard.STORAGE_KEY in hass_storage @@ -120,7 +120,7 @@ async def test_new_info_reload_config_entries( hass: HomeAssistant, init_integration, mock_dashboard ) -> None: """Test config entries are reloaded when new info is set.""" - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup: await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) @@ -156,7 +156,7 @@ async def test_new_dashboard_fix_reauth( "unique_id": mock_config_entry.unique_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert len(mock_get_encryption_key.mock_calls) == 0 diff --git a/tests/components/esphome/test_datetime.py b/tests/components/esphome/test_datetime.py new file mode 100644 index 00000000000..3bdc196de95 --- /dev/null +++ b/tests/components/esphome/test_datetime.py @@ -0,0 +1,79 @@ +"""Test ESPHome datetimes.""" + +from unittest.mock import call + +from aioesphomeapi import APIClient, DateTimeInfo, DateTimeState + +from homeassistant.components.datetime import ( + ATTR_DATETIME, + DOMAIN as DATETIME_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + + +async def test_generic_datetime_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic datetime entity.""" + entity_info = [ + DateTimeInfo( + object_id="mydatetime", + key=1, + name="my datetime", + unique_id="my_datetime", + ) + ] + states = [DateTimeState(key=1, epoch_seconds=1713270896)] + 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("datetime.test_mydatetime") + assert state is not None + assert state.state == "2024-04-16T12:34:56+00:00" + + await hass.services.async_call( + DATETIME_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "datetime.test_mydatetime", + ATTR_DATETIME: "2000-01-01T01:23:45+00:00", + }, + blocking=True, + ) + mock_client.datetime_command.assert_has_calls([call(1, 946689825)]) + mock_client.datetime_command.reset_mock() + + +async def test_generic_datetime_missing_state( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic datetime entity with missing state.""" + entity_info = [ + DateTimeInfo( + object_id="mydatetime", + key=1, + name="my datetime", + unique_id="my_datetime", + ) + ] + states = [DateTimeState(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("datetime.test_mydatetime") + 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 0f2b18218ff..1cf4f77875f 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -94,7 +94,8 @@ async def test_diagnostics_with_bluetooth( "project_version": "", "suggested_area": "", "uses_password": False, - "voice_assistant_version": 0, + "legacy_voice_assistant_version": 0, + "voice_assistant_feature_flags": 0, "webserver_port": 0, }, "services": [], diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 303d50f3103..bc633d87fae 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -23,12 +23,9 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from .conftest import MockESPHomeDevice diff --git a/tests/components/esphome/test_event.py b/tests/components/esphome/test_event.py new file mode 100644 index 00000000000..c17dc4d98a9 --- /dev/null +++ b/tests/components/esphome/test_event.py @@ -0,0 +1,38 @@ +"""Test ESPHome Events.""" + +from aioesphomeapi import APIClient, Event, EventInfo +import pytest + +from homeassistant.components.event import EventDeviceClass +from homeassistant.core import HomeAssistant + + +@pytest.mark.freeze_time("2024-04-24 00:00:00+00:00") +async def test_generic_event_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic event entity.""" + entity_info = [ + EventInfo( + object_id="myevent", + key=1, + name="my event", + unique_id="my_event", + event_types=["type1", "type2"], + device_class=EventDeviceClass.BUTTON, + ) + ] + states = [Event(key=1, event_type="type1")] + 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("event.test_myevent") + assert state is not None + assert state.state == "2024-04-24T00:00:00.000+00:00" + assert state.attributes["event_type"] == "type1" diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 55369e54b53..e62c85b7f9a 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -11,6 +11,9 @@ from aioesphomeapi import ( EntityInfo, EntityState, HomeassistantServiceCall, + InvalidAuthAPIError, + InvalidEncryptionKeyAPIError, + RequiresEncryptionAPIError, UserService, UserServiceArg, UserServiceArgType, @@ -25,7 +28,12 @@ from homeassistant.components.esphome.const import ( DOMAIN, STABLE_BLE_VERSION_STR, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + EVENT_HOMEASSISTANT_CLOSE, +) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, issue_registry as ir @@ -599,7 +607,7 @@ async def test_connection_aborted_wrong_device( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.184" await hass.async_block_till_done() @@ -1083,3 +1091,64 @@ async def test_esphome_device_with_compilation_time( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) assert "comp_time" in dev.sw_version + + +async def test_disconnects_at_close_event( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test the device is disconnected at the close event.""" + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + device_info={"compilation_time": "comp_time"}, + states=[], + ) + await hass.async_block_till_done() + + assert mock_client.disconnect.call_count == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + assert mock_client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "error", + [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ], +) +async def test_start_reauth( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + error: Exception, +) -> None: + """Test exceptions on connect error trigger reauth.""" + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + device_info={"compilation_time": "comp_time"}, + states=[], + ) + await hass.async_block_till_done() + + await device.mock_connect_error(error("fail")) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 1 + flow = flows[0] + assert flow["context"]["source"] == "reauth" diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index ffbe8f50e48..8a3630b92a4 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -1,6 +1,6 @@ """Test ESPHome media_players.""" -from unittest.mock import AsyncMock, Mock, call +from unittest.mock import AsyncMock, Mock, call, patch from aioesphomeapi import ( APIClient, @@ -152,6 +152,8 @@ async def test_media_player_entity_with_source( mock_generic_device_entry, ) -> None: """Test a generic media_player entity media source.""" + await async_setup_component(hass, "media_source", {"media_source": {}}) + await hass.async_block_till_done() esphome_platform_mock = Mock( async_get_media_browser_root_object=AsyncMock( return_value=[ @@ -221,6 +223,33 @@ async def test_media_player_entity_with_source( ) mock_client.media_player_command.reset_mock() + + play_media = media_source.PlayMedia( + url="http://www.example.com/xy.mp3", + mime_type="audio/mp3", + ) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=play_media, + ): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_MEDIA_CONTENT_TYPE: "audio/mp3", + ATTR_MEDIA_CONTENT_ID: "media-source://local/xy", + }, + blocking=True, + ) + + mock_client.media_player_command.assert_has_calls( + [call(1, media_url="http://www.example.com/xy.mp3")] + ) + client = await hass_ws_client() await client.send_json( { diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 959ad12876d..b3deb2f33ee 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,7 +1,6 @@ """Test ESPHome update entities.""" from collections.abc import Awaitable, Callable -import dataclasses from unittest.mock import Mock, patch from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService @@ -18,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import MockESPHomeDevice @@ -176,9 +174,11 @@ async def test_update_entity( async def test_update_static_info( hass: HomeAssistant, - stub_reconnect, - mock_config_entry, - mock_device_info, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], mock_dashboard, ) -> None: """Test ESPHome update entity.""" @@ -190,32 +190,25 @@ async def test_update_static_info( ] await async_get_dashboard(hass).async_refresh() - signal_static_info_updated = f"esphome_{mock_config_entry.entry_id}_on_list" - runtime_data = Mock( - available=True, - device_info=mock_device_info, - signal_static_info_updated=signal_static_info_updated, + mock_device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], ) - 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" + assert state.state == STATE_ON - runtime_data.device_info = dataclasses.replace( - runtime_data.device_info, esphome_version="1.2.3" - ) - async_dispatcher_send(hass, signal_static_info_updated, []) + object.__setattr__(mock_device.device_info, "esphome_version", "1.2.3") + await mock_device.mock_disconnect(True) + await mock_device.mock_connect() - state = hass.states.get("update.none_firmware") - assert state.state == "off" + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("update.test_firmware") + assert state.state == STATE_OFF @pytest.mark.parametrize( diff --git a/tests/components/esphome/test_valve.py b/tests/components/esphome/test_valve.py new file mode 100644 index 00000000000..5ba7bcbe187 --- /dev/null +++ b/tests/components/esphome/test_valve.py @@ -0,0 +1,196 @@ +"""Test ESPHome valves.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import call + +from aioesphomeapi import ( + APIClient, + EntityInfo, + EntityState, + UserService, + ValveInfo, + ValveOperation, + ValveState, +) + +from homeassistant.components.valve import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + SERVICE_SET_VALVE_POSITION, + SERVICE_STOP_VALVE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import MockESPHomeDevice + + +async def test_valve_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a generic valve entity.""" + entity_info = [ + ValveInfo( + object_id="myvalve", + key=1, + name="my valve", + unique_id="my_valve", + supports_position=True, + supports_stop=True, + ) + ] + states = [ + ValveState( + key=1, + position=0.5, + current_operation=ValveOperation.IS_OPENING, + ) + ] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_OPENING + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_SET_VALVE_POSITION, + {ATTR_ENTITY_ID: "valve.test_myvalve", ATTR_POSITION: 50}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=0.5)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_STOP_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, stop=True)]) + mock_client.valve_command.reset_mock() + + mock_device.set_state( + ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_CLOSED + + mock_device.set_state( + ValveState(key=1, position=0.5, current_operation=ValveOperation.IS_CLOSING) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_CLOSING + + mock_device.set_state( + ValveState(key=1, position=1.0, current_operation=ValveOperation.IDLE) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_OPEN + + +async def test_valve_entity_without_position( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a generic valve entity without position or stop.""" + entity_info = [ + ValveInfo( + object_id="myvalve", + key=1, + name="my valve", + unique_id="my_valve", + supports_position=False, + supports_stop=False, + ) + ] + states = [ + ValveState( + key=1, + position=0.5, + current_operation=ValveOperation.IS_OPENING, + ) + ] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_OPENING + assert ATTR_CURRENT_POSITION not in state.attributes + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) + mock_client.valve_command.reset_mock() + + mock_device.set_state( + ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_CLOSED diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 427cd1dbc8f..e67d833656e 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -6,7 +6,7 @@ import socket from unittest.mock import Mock, patch import wave -from aioesphomeapi import VoiceAssistantEventType +from aioesphomeapi import APIClient, VoiceAssistantEventType import pytest from homeassistant.components.assist_pipeline import ( @@ -19,7 +19,10 @@ from homeassistant.components.assist_pipeline.error import ( WakeWordDetectionError, ) from homeassistant.components.esphome import DomainData -from homeassistant.components.esphome.voice_assistant import VoiceAssistantUDPServer +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.core import HomeAssistant _TEST_INPUT_TEXT = "This is an input test" @@ -31,43 +34,54 @@ _ONE_SECOND = 16000 * 2 # 16Khz 16-bit @pytest.fixture -def voice_assistant_udp_server( +def voice_assistant_udp_pipeline( hass: HomeAssistant, -) -> VoiceAssistantUDPServer: - """Return the UDP server factory.""" +) -> VoiceAssistantUDPPipeline: + """Return the UDP pipeline factory.""" def _voice_assistant_udp_server(entry): entry_data = DomainData.get(hass).get_entry_data(entry) - server: VoiceAssistantUDPServer = None + server: VoiceAssistantUDPPipeline = None def handle_finished(): nonlocal server assert server is not None server.close() - server = VoiceAssistantUDPServer(hass, entry_data, Mock(), handle_finished) - return server + server = VoiceAssistantUDPPipeline(hass, entry_data, Mock(), handle_finished) + return server # noqa: RET504 return _voice_assistant_udp_server @pytest.fixture -def voice_assistant_udp_server_v1( - voice_assistant_udp_server, - mock_voice_assistant_v1_entry, -) -> VoiceAssistantUDPServer: - """Return the UDP server.""" - return voice_assistant_udp_server(entry=mock_voice_assistant_v1_entry) +def voice_assistant_api_pipeline( + hass: HomeAssistant, + mock_client, + mock_voice_assistant_api_entry, +) -> VoiceAssistantAPIPipeline: + """Return the API Pipeline factory.""" + entry_data = DomainData.get(hass).get_entry_data(mock_voice_assistant_api_entry) + return VoiceAssistantAPIPipeline(hass, entry_data, Mock(), Mock(), mock_client) @pytest.fixture -def voice_assistant_udp_server_v2( - voice_assistant_udp_server, +def voice_assistant_udp_pipeline_v1( + voice_assistant_udp_pipeline, + mock_voice_assistant_v1_entry, +) -> VoiceAssistantUDPPipeline: + """Return the UDP pipeline.""" + return voice_assistant_udp_pipeline(entry=mock_voice_assistant_v1_entry) + + +@pytest.fixture +def voice_assistant_udp_pipeline_v2( + voice_assistant_udp_pipeline, mock_voice_assistant_v2_entry, -) -> VoiceAssistantUDPServer: - """Return the UDP server.""" - return voice_assistant_udp_server(entry=mock_voice_assistant_v2_entry) +) -> VoiceAssistantUDPPipeline: + """Return the UDP pipeline.""" + return voice_assistant_udp_pipeline(entry=mock_voice_assistant_v2_entry) @pytest.fixture @@ -85,7 +99,7 @@ def test_wav() -> bytes: async def test_pipeline_events( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the pipeline function is called.""" @@ -145,15 +159,15 @@ async def test_pipeline_events( elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: assert data is None - voice_assistant_udp_server_v1.handle_event = handle_event + voice_assistant_udp_pipeline_v1.handle_event = handle_event with patch( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - voice_assistant_udp_server_v1.transport = Mock() + voice_assistant_udp_pipeline_v1.transport = Mock() - await voice_assistant_udp_server_v1.run_pipeline( + await voice_assistant_udp_pipeline_v1.run_pipeline( device_id="mock-device-id", conversation_id=None ) @@ -162,7 +176,7 @@ async def test_udp_server( hass: HomeAssistant, socket_enabled, unused_udp_port_factory, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server runs and queues incoming data.""" port_to_use = unused_udp_port_factory() @@ -170,93 +184,133 @@ async def test_udp_server( with patch( "homeassistant.components.esphome.voice_assistant.UDP_PORT", new=port_to_use ): - port = await voice_assistant_udp_server_v1.start_server() + port = await voice_assistant_udp_pipeline_v1.start_server() assert port == port_to_use sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - assert voice_assistant_udp_server_v1.queue.qsize() == 0 + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 0 sock.sendto(b"test", ("127.0.0.1", port)) # Give the socket some time to send/receive the data async with asyncio.timeout(1): - while voice_assistant_udp_server_v1.queue.qsize() == 0: + while voice_assistant_udp_pipeline_v1.queue.qsize() == 0: await asyncio.sleep(0.1) - assert voice_assistant_udp_server_v1.queue.qsize() == 1 + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 1 - voice_assistant_udp_server_v1.stop() - voice_assistant_udp_server_v1.close() + voice_assistant_udp_pipeline_v1.stop() + voice_assistant_udp_pipeline_v1.close() - assert voice_assistant_udp_server_v1.transport.is_closing() + assert voice_assistant_udp_pipeline_v1.transport.is_closing() async def test_udp_server_queue( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server queues incoming data.""" - voice_assistant_udp_server_v1.started = True + voice_assistant_udp_pipeline_v1.started = True - assert voice_assistant_udp_server_v1.queue.qsize() == 0 + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 0 - voice_assistant_udp_server_v1.datagram_received(bytes(1024), ("localhost", 0)) - assert voice_assistant_udp_server_v1.queue.qsize() == 1 + voice_assistant_udp_pipeline_v1.datagram_received(bytes(1024), ("localhost", 0)) + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 1 - voice_assistant_udp_server_v1.datagram_received(bytes(1024), ("localhost", 0)) - assert voice_assistant_udp_server_v1.queue.qsize() == 2 + voice_assistant_udp_pipeline_v1.datagram_received(bytes(1024), ("localhost", 0)) + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 2 - async for data in voice_assistant_udp_server_v1._iterate_packets(): + async for data in voice_assistant_udp_pipeline_v1._iterate_packets(): assert data == bytes(1024) break - assert voice_assistant_udp_server_v1.queue.qsize() == 1 # One message removed + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 1 # One message removed - voice_assistant_udp_server_v1.stop() + voice_assistant_udp_pipeline_v1.stop() assert ( - voice_assistant_udp_server_v1.queue.qsize() == 2 + voice_assistant_udp_pipeline_v1.queue.qsize() == 2 ) # An empty message added by stop - voice_assistant_udp_server_v1.datagram_received(bytes(1024), ("localhost", 0)) + voice_assistant_udp_pipeline_v1.datagram_received(bytes(1024), ("localhost", 0)) assert ( - voice_assistant_udp_server_v1.queue.qsize() == 2 + voice_assistant_udp_pipeline_v1.queue.qsize() == 2 ) # No new messages added after stop - voice_assistant_udp_server_v1.close() + voice_assistant_udp_pipeline_v1.close() # Stopping the UDP server should cause _iterate_packets to break out # immediately without yielding any data. has_data = False - async for _data in voice_assistant_udp_server_v1._iterate_packets(): + async for _data in voice_assistant_udp_pipeline_v1._iterate_packets(): has_data = True assert not has_data, "Server was stopped" +async def test_api_pipeline_queue( + hass: HomeAssistant, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the API pipeline queues incoming data.""" + + voice_assistant_api_pipeline.started = True + + assert voice_assistant_api_pipeline.queue.qsize() == 0 + + voice_assistant_api_pipeline.receive_audio_bytes(bytes(1024)) + assert voice_assistant_api_pipeline.queue.qsize() == 1 + + voice_assistant_api_pipeline.receive_audio_bytes(bytes(1024)) + assert voice_assistant_api_pipeline.queue.qsize() == 2 + + async for data in voice_assistant_api_pipeline._iterate_packets(): + assert data == bytes(1024) + break + assert voice_assistant_api_pipeline.queue.qsize() == 1 # One message removed + + voice_assistant_api_pipeline.stop() + assert ( + voice_assistant_api_pipeline.queue.qsize() == 2 + ) # An empty message added by stop + + voice_assistant_api_pipeline.receive_audio_bytes(bytes(1024)) + assert ( + voice_assistant_api_pipeline.queue.qsize() == 2 + ) # No new messages added after stop + + # Stopping the API Pipeline should cause _iterate_packets to break out + # immediately without yielding any data. + has_data = False + async for _data in voice_assistant_api_pipeline._iterate_packets(): + has_data = True + + assert not has_data, "Pipeline was stopped" + + async def test_error_calls_handle_finished( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the handle_finished callback is called when an error occurs.""" - voice_assistant_udp_server_v1.handle_finished = Mock() + voice_assistant_udp_pipeline_v1.handle_finished = Mock() - voice_assistant_udp_server_v1.error_received(Exception()) + voice_assistant_udp_pipeline_v1.error_received(Exception()) - voice_assistant_udp_server_v1.handle_finished.assert_called() + voice_assistant_udp_pipeline_v1.handle_finished.assert_called() async def test_udp_server_multiple( hass: HomeAssistant, socket_enabled, unused_udp_port_factory, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the UDP server raises an error if started twice.""" with patch( "homeassistant.components.esphome.voice_assistant.UDP_PORT", new=unused_udp_port_factory(), ): - await voice_assistant_udp_server_v1.start_server() + await voice_assistant_udp_pipeline_v1.start_server() with ( patch( @@ -265,17 +319,17 @@ async def test_udp_server_multiple( ), pytest.raises(RuntimeError), ): - await voice_assistant_udp_server_v1.start_server() + await voice_assistant_udp_pipeline_v1.start_server() async def test_udp_server_after_stopped( hass: HomeAssistant, socket_enabled, unused_udp_port_factory, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the UDP server raises an error if started after stopped.""" - voice_assistant_udp_server_v1.close() + voice_assistant_udp_pipeline_v1.close() with ( patch( "homeassistant.components.esphome.voice_assistant.UDP_PORT", @@ -283,37 +337,37 @@ async def test_udp_server_after_stopped( ), pytest.raises(RuntimeError), ): - await voice_assistant_udp_server_v1.start_server() + await voice_assistant_udp_pipeline_v1.start_server() async def test_unknown_event_type( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server does not call handle_event for unknown events.""" - voice_assistant_udp_server_v1._event_callback( + """Test the API pipeline does not call handle_event for unknown events.""" + voice_assistant_api_pipeline._event_callback( PipelineEvent( type="unknown-event", data={}, ) ) - assert not voice_assistant_udp_server_v1.handle_event.called + assert not voice_assistant_api_pipeline.handle_event.called async def test_error_event_type( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server calls event handler with error.""" - voice_assistant_udp_server_v1._event_callback( + """Test the API pipeline calls event handler with error.""" + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.ERROR, data={"code": "code", "message": "message"}, ) ) - voice_assistant_udp_server_v1.handle_event.assert_called_with( + voice_assistant_api_pipeline.handle_event.assert_called_with( VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, {"code": "code", "message": "message"}, ) @@ -321,13 +375,13 @@ async def test_error_event_type( async def test_send_tts_not_called( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server with a v1 device does not call _send_tts.""" with patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" ) as mock_send_tts: - voice_assistant_udp_server_v1._event_callback( + voice_assistant_udp_pipeline_v1._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -339,15 +393,35 @@ async def test_send_tts_not_called( mock_send_tts.assert_not_called() -async def test_send_tts_called( +async def test_send_tts_called_udp( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server with a v2 device calls _send_tts.""" with patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" ) as mock_send_tts: - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} + }, + ) + ) + + mock_send_tts.assert_called_with(_TEST_MEDIA_ID) + + +async def test_send_tts_called_api( + hass: HomeAssistant, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the API pipeline calls _send_tts.""" + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" + ) as mock_send_tts: + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -361,29 +435,36 @@ async def test_send_tts_called( async def test_send_tts_not_called_when_empty( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server with a v1/v2 device doesn't call _send_tts when the output is empty.""" + """Test the pipelines do not call _send_tts when the output is empty.""" with patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" ) as mock_send_tts: - voice_assistant_udp_server_v1._event_callback( + voice_assistant_udp_pipeline_v1._event_callback( PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) ) mock_send_tts.assert_not_called() - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( + PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) + ) + + mock_send_tts.assert_not_called() + + voice_assistant_api_pipeline._event_callback( PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) ) mock_send_tts.assert_not_called() -async def test_send_tts( +async def test_send_tts_udp( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, test_wav, ) -> None: """Test the UDP server calls sendto to transmit audio data to device.""" @@ -391,12 +472,12 @@ async def test_send_tts( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", return_value=("wav", test_wav), ): - voice_assistant_udp_server_v2.started = True - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_udp_pipeline_v2.started = True + voice_assistant_udp_pipeline_v2.transport = Mock(spec=asyncio.DatagramTransport) with patch.object( - voice_assistant_udp_server_v2.transport, "is_closing", return_value=False + voice_assistant_udp_pipeline_v2.transport, "is_closing", return_value=False ): - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -408,16 +489,46 @@ async def test_send_tts( ) ) - await voice_assistant_udp_server_v2._tts_done.wait() + await voice_assistant_udp_pipeline_v2._tts_done.wait() - voice_assistant_udp_server_v2.transport.sendto.assert_called() + voice_assistant_udp_pipeline_v2.transport.sendto.assert_called() + + +async def test_send_tts_api( + hass: HomeAssistant, + mock_client: APIClient, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, + test_wav, +) -> None: + """Test the API pipeline calls cli.send_voice_assistant_audio to transmit audio data to device.""" + with patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", test_wav), + ): + voice_assistant_api_pipeline.started = True + + voice_assistant_api_pipeline._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": { + "media_id": _TEST_MEDIA_ID, + "url": _TEST_OUTPUT_URL, + } + }, + ) + ) + + await voice_assistant_api_pipeline._tts_done.wait() + + mock_client.send_voice_assistant_audio.assert_called() async def test_send_tts_wrong_sample_rate( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server calls sendto to transmit audio data to device.""" + """Test that only 16000Hz audio will be streamed.""" with io.BytesIO() as wav_io: with wave.open(wav_io, "wb") as wav_file: wav_file.setframerate(22050) @@ -433,10 +544,10 @@ async def test_send_tts_wrong_sample_rate( ), pytest.raises(ValueError), ): - voice_assistant_udp_server_v2.started = True - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_api_pipeline.started = True + voice_assistant_api_pipeline.transport = Mock(spec=asyncio.DatagramTransport) - voice_assistant_udp_server_v2._event_callback( + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -445,13 +556,13 @@ async def test_send_tts_wrong_sample_rate( ) ) - assert voice_assistant_udp_server_v2._tts_task is not None - await voice_assistant_udp_server_v2._tts_task # raises ValueError + assert voice_assistant_api_pipeline._tts_task is not None + await voice_assistant_api_pipeline._tts_task # raises ValueError async def test_send_tts_wrong_format( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that only WAV audio will be streamed.""" with ( @@ -461,10 +572,10 @@ async def test_send_tts_wrong_format( ), pytest.raises(ValueError), ): - voice_assistant_udp_server_v2.started = True - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_api_pipeline.started = True + voice_assistant_api_pipeline.transport = Mock(spec=asyncio.DatagramTransport) - voice_assistant_udp_server_v2._event_callback( + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -473,13 +584,13 @@ async def test_send_tts_wrong_format( ) ) - assert voice_assistant_udp_server_v2._tts_task is not None - await voice_assistant_udp_server_v2._tts_task # raises ValueError + assert voice_assistant_api_pipeline._tts_task is not None + await voice_assistant_api_pipeline._tts_task # raises ValueError async def test_send_tts_not_started( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, test_wav, ) -> None: """Test the UDP server does not call sendto when not started.""" @@ -487,10 +598,10 @@ async def test_send_tts_not_started( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", return_value=("wav", test_wav), ): - voice_assistant_udp_server_v2.started = False - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_udp_pipeline_v2.started = False + voice_assistant_udp_pipeline_v2.transport = Mock(spec=asyncio.DatagramTransport) - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -499,14 +610,41 @@ async def test_send_tts_not_started( ) ) - await voice_assistant_udp_server_v2._tts_done.wait() + await voice_assistant_udp_pipeline_v2._tts_done.wait() - voice_assistant_udp_server_v2.transport.sendto.assert_not_called() + voice_assistant_udp_pipeline_v2.transport.sendto.assert_not_called() + + +async def test_send_tts_transport_none( + hass: HomeAssistant, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, + test_wav, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the UDP server does not call sendto when transport is None.""" + with patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", test_wav), + ): + voice_assistant_udp_pipeline_v2.started = True + voice_assistant_udp_pipeline_v2.transport = None + + voice_assistant_udp_pipeline_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} + }, + ) + ) + await voice_assistant_udp_pipeline_v2._tts_done.wait() + + assert "No transport to send audio to" in caplog.text async def test_wake_word( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that the pipeline is set to start with Wake word.""" @@ -520,9 +658,7 @@ async def test_wake_word( ), patch("asyncio.Event.wait"), # TTS wait event ): - voice_assistant_udp_server_v2.transport = Mock() - - await voice_assistant_udp_server_v2.run_pipeline( + await voice_assistant_api_pipeline.run_pipeline( device_id="mock-device-id", conversation_id=None, flags=2, @@ -531,7 +667,7 @@ async def test_wake_word( async def test_wake_word_exception( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that the pipeline is set to start with Wake word.""" @@ -542,7 +678,6 @@ async def test_wake_word_exception( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - voice_assistant_udp_server_v2.transport = Mock() def handle_event( event_type: VoiceAssistantEventType, data: dict[str, str] | None @@ -552,9 +687,9 @@ async def test_wake_word_exception( assert data["code"] == "pipeline-not-found" assert data["message"] == "Pipeline not found" - voice_assistant_udp_server_v2.handle_event = handle_event + voice_assistant_api_pipeline.handle_event = handle_event - await voice_assistant_udp_server_v2.run_pipeline( + await voice_assistant_api_pipeline.run_pipeline( device_id="mock-device-id", conversation_id=None, flags=2, @@ -563,7 +698,7 @@ async def test_wake_word_exception( async def test_wake_word_abort_exception( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that the pipeline is set to start with Wake word.""" @@ -575,13 +710,9 @@ async def test_wake_word_abort_exception( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ), - patch.object( - voice_assistant_udp_server_v2, "handle_event" - ) as mock_handle_event, + patch.object(voice_assistant_api_pipeline, "handle_event") as mock_handle_event, ): - voice_assistant_udp_server_v2.transport = Mock() - - await voice_assistant_udp_server_v2.run_pipeline( + await voice_assistant_api_pipeline.run_pipeline( device_id="mock-device-id", conversation_id=None, flags=2, diff --git a/tests/components/eufylife_ble/test_config_flow.py b/tests/components/eufylife_ble/test_config_flow.py index c3590077d93..cab70437925 100644 --- a/tests/components/eufylife_ble/test_config_flow.py +++ b/tests/components/eufylife_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Scale C1" assert result2["data"] == {"model": "eufy T9146"} assert result2["result"].unique_id == "11:22:33:44:55:66" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_eufylife(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_EUFYLIFE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "11:22:33:44:55:66"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Scale C1" assert result2["data"] == {"model": "eufy T9146"} assert result2["result"].unique_id == "11:22:33:44:55:66" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "11:22:33:44:55:66"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "11:22:33:44:55:66"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Scale C1" assert result2["data"] == {"model": "eufy T9146"} assert result2["result"].unique_id == "11:22:33:44:55:66" diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index b6bdae940ba..398cfde560a 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -47,7 +47,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Fibonacci256-23D4" assert result2["data"] == { "host": "1.1.1.1", @@ -74,7 +74,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Unable to connect" in caplog.text @@ -96,7 +96,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout"} @@ -117,5 +117,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/evil_genius_labs/test_init.py b/tests/components/evil_genius_labs/test_init.py index 71b8a6164a6..10b773ead61 100644 --- a/tests/components/evil_genius_labs/test_init.py +++ b/tests/components/evil_genius_labs/test_init.py @@ -2,8 +2,8 @@ import pytest -from homeassistant import config_entries from homeassistant.components.evil_genius_labs import PLATFORMS +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -14,4 +14,4 @@ async def test_setup_unload_entry( """Test setting up and unloading a config entry.""" assert len(hass.states.async_entity_ids()) == 1 assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index c99c9c0fe9e..57c3ae0600e 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -51,7 +51,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**API_LOGIN_RETURN_VALIDATE} @@ -71,7 +71,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -90,7 +90,7 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -101,7 +101,7 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == API_LOGIN_RETURN_VALIDATE assert len(mock_setup_entry.mock_calls) == 1 @@ -113,7 +113,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -124,7 +124,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**API_LOGIN_RETURN_VALIDATE} @@ -133,7 +133,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -146,7 +146,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -158,7 +158,7 @@ async def test_step_discovery_abort_if_cloud_account_missing( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -171,7 +171,7 @@ async def test_step_discovery_abort_if_cloud_account_missing( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -181,7 +181,7 @@ async def test_step_reauth_abort_if_cloud_account_missing(hass: HomeAssistant) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -195,7 +195,7 @@ async def test_async_step_integration_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -209,7 +209,7 @@ async def test_async_step_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CAMERA, @@ -228,7 +228,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -238,7 +238,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" assert result["data"][CONF_TIMEOUT] == 25 @@ -250,7 +250,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -261,7 +261,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -272,7 +272,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -283,7 +283,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "mfa_required"} @@ -294,7 +294,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -305,7 +305,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -322,7 +322,7 @@ async def test_discover_exception_step1( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -337,7 +337,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -351,7 +351,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -365,7 +365,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -379,7 +379,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "mfa_required"} @@ -393,7 +393,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -411,7 +411,7 @@ async def test_discover_exception_step3( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -426,7 +426,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -440,7 +440,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -454,7 +454,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -476,7 +476,7 @@ async def test_user_custom_url_exception( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -485,7 +485,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -496,7 +496,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_host"} @@ -507,7 +507,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -518,7 +518,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "mfa_required"} @@ -529,7 +529,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -541,7 +541,7 @@ async def test_async_step_reauth_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -552,7 +552,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**API_LOGIN_RETURN_VALIDATE} @@ -561,7 +561,7 @@ async def test_async_step_reauth_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -575,7 +575,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_host"} @@ -589,7 +589,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_host"} @@ -603,7 +603,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "mfa_required"} @@ -617,7 +617,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -631,5 +631,5 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 92a8929afbf..4420bc73632 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch from aiohttp import ClientConnectionError import faadelays -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.faa_delays.const import DOMAIN from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry @@ -25,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test" assert result2["data"] == { "id": "test", @@ -61,7 +62,7 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -79,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -97,5 +98,5 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 7955a91bc0a..fbc7c7bb1bb 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, + FanEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -25,12 +26,14 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) +from tests.common import MockEntity + async def async_turn_on( hass, entity_id=ENTITY_MATCH_ALL, - percentage: int = None, - preset_mode: str = None, + percentage: int | None = None, + preset_mode: str | None = None, ) -> None: """Turn all or specified fan on.""" data = { @@ -73,7 +76,7 @@ async def async_oscillate( async def async_set_preset_mode( - hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str = None + hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str | None = None ) -> None: """Set preset mode for all or specified fan.""" data = { @@ -87,7 +90,7 @@ async def async_set_preset_mode( async def async_set_percentage( - hass, entity_id=ENTITY_MATCH_ALL, percentage: int = None + hass, entity_id=ENTITY_MATCH_ALL, percentage: int | None = None ) -> None: """Set percentage for all or specified fan.""" data = { @@ -101,7 +104,7 @@ async def async_set_percentage( async def async_increase_speed( - hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None + hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None ) -> None: """Increase speed for all or specified fan.""" data = { @@ -118,7 +121,7 @@ async def async_increase_speed( async def async_decrease_speed( - hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None + hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None ) -> None: """Decrease speed for all or specified fan.""" data = { @@ -135,7 +138,7 @@ async def async_decrease_speed( async def async_set_direction( - hass, entity_id=ENTITY_MATCH_ALL, direction: str = None + hass, entity_id=ENTITY_MATCH_ALL, direction: str | None = None ) -> None: """Set direction for all or specified fan.""" data = { @@ -146,3 +149,16 @@ async def async_set_direction( await hass.services.async_call(DOMAIN, SERVICE_SET_DIRECTION, data, blocking=True) await hass.async_block_till_done() + + +class MockFan(MockEntity, FanEntity): + """Mock Fan class.""" + + @property + def preset_modes(self) -> list[str] | None: + """Return preset mode.""" + return self._handle("preset_modes") + + def set_preset_mode(self, preset_mode: str) -> None: + """Set preset mode.""" + self._attr_preset_mode = preset_mode diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index c08e0617700..96e02ab5592 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index afd237d1974..72e1dfb4ca2 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 92b6443f241..a217a5d89ec 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory @@ -385,15 +385,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 911954d1ecd..e6bcc5542bd 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -16,8 +16,12 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import help_test_all, import_and_test_deprecated_constant_enum -from tests.testing_config.custom_components.test.fan import MockFan +from tests.common import ( + help_test_all, + import_and_test_deprecated_constant_enum, + setup_test_component_platform, +) +from tests.components.fan.common import MockFan class BaseFan(FanEntity): @@ -103,21 +107,21 @@ async def test_preset_mode_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test preset mode validation.""" - await hass.async_block_till_done() - platform = getattr(hass.components, "test.fan") - platform.init(empty=False) + test_fan = MockFan( + name="Support fan with preset_mode support", + supported_features=FanEntityFeature.PRESET_MODE, + unique_id="unique_support_preset_mode", + preset_modes=["auto", "eco"], + ) + setup_test_component_platform(hass, "fan", [test_fan]) assert await async_setup_component(hass, "fan", {"fan": {"platform": "test"}}) await hass.async_block_till_done() - test_fan: MockFan = platform.ENTITIES["support_preset_mode"] - await hass.async_block_till_done() - state = hass.states.get("fan.support_fan_with_preset_mode_support") assert state.attributes.get(ATTR_PRESET_MODES) == ["auto", "eco"] diff --git a/tests/components/fastdotcom/test_config_flow.py b/tests/components/fastdotcom/test_config_flow.py index d2122f4fe61..db28aaec703 100644 --- a/tests/components/fastdotcom/test_config_flow.py +++ b/tests/components/fastdotcom/test_config_flow.py @@ -19,7 +19,7 @@ async def test_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -31,7 +31,7 @@ async def test_user_form(hass: HomeAssistant) -> None: user_input={}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fast.com" assert result["data"] == {} assert result["options"] == {} @@ -52,7 +52,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -66,7 +66,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fast.com" assert result["data"] == {} assert result["options"] == {} diff --git a/tests/components/fastdotcom/test_init.py b/tests/components/fastdotcom/test_init.py index 12e3902d874..c17b455057b 100644 --- a/tests/components/fastdotcom/test_init.py +++ b/tests/components/fastdotcom/test_init.py @@ -6,8 +6,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory -from homeassistant import config_entries from homeassistant.components.fastdotcom.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, STATE_UNKNOWN from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import issue_registry as ir @@ -31,10 +31,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_from_import(hass: HomeAssistant) -> None: @@ -72,7 +72,7 @@ async def test_delayed_speedtest_during_startup( await hass.async_block_till_done() hass.set_state(original_state) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.fast_com_download") # Assert state is Unknown as fast.com isn't starting until HA has started assert state.state is STATE_UNKNOWN @@ -87,7 +87,7 @@ async def test_delayed_speedtest_during_startup( assert state is not None assert state.state == "5.0" - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_service_deprecated( diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index f836d233670..67ce95811a0 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -36,8 +36,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, DOMAIN) - raw = bytes(feed_data, "utf-8") - return raw + return bytes(feed_data, "utf-8") @pytest.fixture(name="feed_one_event") diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 2c7d05b87a3..b6b4e3992cd 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -35,7 +35,7 @@ async def _recovery_after_failure_works( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_URL: TEST_URL, @@ -56,7 +56,7 @@ async def _recovery_after_reauth_failure_works( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -66,7 +66,7 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -79,7 +79,7 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_URL: TEST_URL, @@ -89,36 +89,6 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None: } -async def test_config_flow_user_initiated_connect_failure( - hass: HomeAssistant, mock_fibaro_client: Mock -) -> None: - """Connect failure in flow manually initialized by the user.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - mock_fibaro_client.connect.return_value = False - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_URL: TEST_URL, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, - ) - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} - - await _recovery_after_failure_works(hass, mock_fibaro_client, result) - - async def test_config_flow_user_initiated_auth_failure( hass: HomeAssistant, mock_fibaro_client: Mock ) -> None: @@ -127,7 +97,7 @@ async def test_config_flow_user_initiated_auth_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -142,7 +112,7 @@ async def test_config_flow_user_initiated_auth_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -157,7 +127,7 @@ async def test_config_flow_user_initiated_unknown_failure_1( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -172,7 +142,7 @@ async def test_config_flow_user_initiated_unknown_failure_1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -187,7 +157,7 @@ async def test_config_flow_user_initiated_unknown_failure_2( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -202,7 +172,7 @@ async def test_config_flow_user_initiated_unknown_failure_2( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -222,7 +192,7 @@ async def test_reauth_success( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -231,7 +201,7 @@ async def test_reauth_success( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -250,7 +220,7 @@ async def test_reauth_connect_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -261,7 +231,7 @@ async def test_reauth_connect_failure( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -283,7 +253,7 @@ async def test_reauth_auth_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -294,7 +264,7 @@ async def test_reauth_auth_failure( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 38209a3014e..4b275e66d02 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -27,7 +27,7 @@ async def test_full_user_flow(hass: HomeAssistant, tmp_path: Path) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -35,7 +35,7 @@ async def test_full_user_flow(hass: HomeAssistant, tmp_path: Path) -> None: user_input={CONF_FILE_PATH: test_file}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_FILE_NAME assert result2.get("data") == {CONF_FILE_PATH: test_file} @@ -53,7 +53,7 @@ async def test_unique_path( DOMAIN, context={"source": SOURCE_USER}, data={CONF_FILE_PATH: test_file} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -66,7 +66,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant, tmp_path: Path) -> DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -103,7 +103,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant, tmp_path: Path) -> }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_FILE_NAME assert result2["data"] == { CONF_FILE_PATH: test_file, diff --git a/tests/components/fints/test_client.py b/tests/components/fints/test_client.py index 429d391b07e..398d539d5b9 100644 --- a/tests/components/fints/test_client.py +++ b/tests/components/fints/test_client.py @@ -1,7 +1,5 @@ """Tests for the FinTS client.""" -from typing import Optional - from fints.client import BankIdentifier, FinTSOperations import pytest @@ -51,10 +49,10 @@ BANK_INFORMATION = { ], ) async def test_account_type( - account_number: Optional[str], - iban: Optional[str], + account_number: str | None, + iban: str | None, product_name: str, - account_type: Optional[int], + account_type: int | None, expected_balance_result: bool, expected_holdings_result: bool, ) -> None: diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index e2bf5911089..539906d800b 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pyfireservicerota import InvalidAuthError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.fireservicerota.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_show_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -57,7 +58,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -95,7 +96,7 @@ async def test_step_user(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_CONF[CONF_USERNAME] assert result["data"] == { "auth_implementation": "fireservicerota", @@ -135,7 +136,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -154,5 +155,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index a3c8ca7e728..0ef98a27bb6 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.firmata.const import CONF_SERIAL_PORT, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: @@ -23,7 +24,7 @@ async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -40,7 +41,7 @@ async def test_import_cannot_connect_serial(hass: HomeAssistant) -> None: data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -57,7 +58,7 @@ async def test_import_cannot_connect_serial_timeout(hass: HomeAssistant) -> None data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -79,7 +80,7 @@ async def test_import(hass: HomeAssistant) -> None: data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "serial-/dev/nonExistent" assert result["data"] == { CONF_NAME: "serial-/dev/nonExistent", diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index 78d20b0fb58..843a85dec68 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -51,7 +51,7 @@ async def test_full_flow( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -118,7 +118,7 @@ async def test_token_error( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -137,7 +137,7 @@ async def test_token_error( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error_reason @@ -177,7 +177,7 @@ async def test_api_failure( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -203,7 +203,7 @@ async def test_api_failure( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error_reason @@ -233,7 +233,7 @@ async def test_config_entry_already_exists( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -252,7 +252,7 @@ async def test_config_entry_already_exists( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -480,14 +480,14 @@ async def test_reauth_flow( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -522,7 +522,7 @@ async def test_reauth_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -554,14 +554,14 @@ async def test_reauth_wrong_user_id( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -596,7 +596,7 @@ async def test_reauth_wrong_user_id( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "wrong_account" assert len(mock_setup.mock_calls) == 0 @@ -630,7 +630,7 @@ async def test_partial_profile_data( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" diff --git a/tests/components/fitbit/test_init.py b/tests/components/fitbit/test_init.py index 74312348af1..a4794a63162 100644 --- a/tests/components/fitbit/test_init.py +++ b/tests/components/fitbit/test_init.py @@ -34,15 +34,15 @@ async def test_setup( setup_credentials: None, ) -> None: """Test setting up the integration.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -68,7 +68,7 @@ async def test_token_refresh_failure( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize("token_expiration_time", [12345]) @@ -88,7 +88,7 @@ async def test_token_refresh_success( ) assert await integration_setup() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Verify token request assert len(aioclient_mock.mock_calls) == 1 @@ -132,7 +132,7 @@ async def test_token_requires_reauth( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -147,7 +147,7 @@ async def test_device_update_coordinator_failure( requests_mock: Mocker, ) -> None: """Test case where the device update coordinator fails on the first request.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED requests_mock.register_uri( "GET", @@ -156,7 +156,7 @@ async def test_device_update_coordinator_failure( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_device_update_coordinator_reauth( @@ -167,7 +167,7 @@ async def test_device_update_coordinator_reauth( requests_mock: Mocker, ) -> None: """Test case where the device update coordinator fails on the first request.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED requests_mock.register_uri( "GET", @@ -179,7 +179,7 @@ async def test_device_update_coordinator_reauth( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py index 174078c5420..2189a6ec34b 100644 --- a/tests/components/fivem/test_config_flow.py +++ b/tests/components/fivem/test_config_flow.py @@ -55,7 +55,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -64,7 +64,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -83,7 +83,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_HOST] assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -105,7 +105,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -125,7 +125,7 @@ async def test_form_invalid(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -145,5 +145,5 @@ async def test_form_invalid_game_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_game_name"} diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index b6da1fcf5b5..fa0df9241dd 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -34,10 +34,10 @@ async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fjäråskupan" assert result["data"] == {} @@ -56,8 +56,8 @@ async def test_scan_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index fb4b2237833..d7e7962003b 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -22,7 +22,7 @@ async def flow_id(hass: HomeAssistant) -> str: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} return result["flow_id"] diff --git a/tests/components/flexit_bacnet/test_config_flow.py b/tests/components/flexit_bacnet/test_config_flow.py index 7d864a80c2d..77895ac647e 100644 --- a/tests/components/flexit_bacnet/test_config_flow.py +++ b/tests/components/flexit_bacnet/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Device Name" assert result["context"]["unique_id"] == "0000-0001" assert result["data"] == { @@ -67,7 +67,7 @@ async def test_flow_fails( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": message} assert len(mock_setup_entry.mock_calls) == 0 @@ -81,7 +81,7 @@ async def test_flow_fails( }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Device Name" assert result2["context"]["unique_id"] == "0000-0001" assert result2["data"] == { @@ -105,5 +105,5 @@ async def test_form_device_already_exist( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/flexit_bacnet/test_init.py b/tests/components/flexit_bacnet/test_init.py index 0741120c1ad..4ff52a3bcfc 100644 --- a/tests/components/flexit_bacnet/test_init.py +++ b/tests/components/flexit_bacnet/test_init.py @@ -18,7 +18,7 @@ async def test_loading_and_unloading_config_entry( await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -33,4 +33,4 @@ async def test_failed_initialization( mock_flexit_bacnet.update.side_effect = DecodingError mock_config_entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 9635f3a1526..1b3ed1de34d 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pyflick.authentication import AuthException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.flick_electric.const import DOMAIN 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 @@ -28,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -47,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Flick Electric: test-username" assert result2["data"] == CONF assert len(mock_setup_entry.mock_calls) == 1 @@ -69,7 +70,7 @@ async def test_form_duplicate_login(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -81,7 +82,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -93,7 +94,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -105,5 +106,5 @@ async def test_form_generic_exception(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 4ee6d85cead..b99e6af7383 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch import pytest from requests.exceptions import HTTPError, Timeout -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(name="mock_setup") @@ -27,7 +28,7 @@ async def test_show_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -46,7 +47,7 @@ async def test_invalid_credential(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -69,7 +70,7 @@ async def test_nominal_case(hass: HomeAssistant, mock_setup) -> None: assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "flipid" assert result["data"] == { CONF_EMAIL: "dummylogin", @@ -93,7 +94,7 @@ async def test_multiple_flip_id(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "flipr_id" result = await hass.config_entries.flow.async_configure( @@ -103,7 +104,7 @@ async def test_multiple_flip_id(hass: HomeAssistant, mock_setup) -> None: assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "FLIP2" assert result["data"] == { CONF_EMAIL: "dummylogin", @@ -128,7 +129,7 @@ async def test_no_flip_id(hass: HomeAssistant, mock_setup) -> None: ) assert result["step_id"] == "user" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_flipr_id_found"} assert len(mock_flipr_client.mock_calls) == 1 @@ -147,7 +148,7 @@ async def test_http_errors(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -164,5 +165,5 @@ async def test_http_errors(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_init.py b/tests/components/flipr/test_init.py index 8300ac185ba..6a49b5b7200 100644 --- a/tests/components/flipr/test_init.py +++ b/tests/components/flipr/test_init.py @@ -26,4 +26,4 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index f5a730a2056..99f8f315fb2 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.flo.const import DOMAIN from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID @@ -21,7 +22,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -31,7 +32,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: result["flow_id"], {"username": TEST_USER_ID, "password": TEST_PASSWORD} ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USER_ID assert result2["data"] == {"username": TEST_USER_ID, "password": TEST_PASSWORD} await hass.async_block_till_done() @@ -68,5 +69,5 @@ async def test_form_cannot_connect( result["flow_id"], {"username": "test-username", "password": "test-password"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 8fa66c03258..706cee44739 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_flume_device_list = _get_mocked_flume_device_list() @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -96,7 +97,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -125,7 +126,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -147,7 +148,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -167,7 +168,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} with ( @@ -187,7 +188,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} mock_flume_device_list = _get_mocked_flume_device_list() @@ -214,5 +215,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) assert mock_setup_entry.called - assert result4["type"] == "abort" + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "reauth_successful" diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index a3eeec10fa5..018d1c43b70 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1115,7 +1115,7 @@ async def test_flux_with_mired( hass: HomeAssistant, mock_light_entities: list[MockLight], ) -> None: - """Test the flux switch´s mode mired.""" + """Test the flux switch's mode mired.""" setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) assert await async_setup_component( @@ -1176,7 +1176,7 @@ async def test_flux_with_rgb( hass: HomeAssistant, mock_light_entities: list[MockLight], ) -> None: - """Test the flux switch´s mode rgb.""" + """Test the flux switch's mode rgb.""" setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) assert await async_setup_component( diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 63a7a671871..d95bc99f097 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -55,13 +55,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -69,13 +69,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -91,7 +91,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_MINOR_VERSION: 4, @@ -111,7 +111,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -119,7 +119,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -130,13 +130,13 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -144,13 +144,13 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -166,7 +166,7 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_MINOR_VERSION: 4, @@ -186,7 +186,7 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -194,7 +194,7 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -212,7 +212,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -220,7 +220,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -229,7 +229,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -237,7 +237,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -249,7 +249,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: MAC_ADDRESS} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_MINOR_VERSION: 4, @@ -270,7 +270,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -278,7 +278,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -292,7 +292,7 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -301,7 +301,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -312,7 +312,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -327,7 +327,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == DEFAULT_ENTRY_TITLE assert result4["data"] == { CONF_MINOR_VERSION: 4, @@ -351,7 +351,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -360,7 +360,7 @@ async def test_manual_no_discovery_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -375,7 +375,7 @@ async def test_manual_no_discovery_data(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, @@ -393,7 +393,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=FLUX_DISCOVERY, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(): @@ -403,7 +403,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_wifibulb(): @@ -417,7 +417,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -432,7 +432,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -446,7 +446,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, @@ -471,7 +471,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -485,7 +485,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, @@ -510,7 +510,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -524,7 +524,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, @@ -545,7 +545,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -559,7 +559,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, @@ -581,7 +581,7 @@ async def test_discovered_by_dhcp_no_udp_response_or_tcp_response( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -605,7 +605,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -628,7 +628,7 @@ async def test_mac_address_off_by_one_updated_via_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -649,7 +649,7 @@ async def test_mac_address_off_by_one_not_updated_from_dhcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF @@ -677,7 +677,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT @@ -703,7 +703,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" user_input = { @@ -716,7 +716,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == user_input assert result2["data"] == config_entry.options assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None @@ -745,5 +745,5 @@ async def test_discovered_can_be_ignored(hass: HomeAssistant, source, data) -> N ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index a42ba5dff37..8e3bb03dca2 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -98,10 +98,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_discovery(), _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 + assert config_entry.state is 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 + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry(hass: HomeAssistant) -> None: @@ -113,7 +113,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) -> None: @@ -125,7 +125,7 @@ async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) - with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY with _patch_discovery(), _patch_wifibulb(): await hass.config_entries.flow.async_init( @@ -134,7 +134,7 @@ async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) - data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_coordinator_retry_right_away_on_discovery_already_setup( @@ -152,7 +152,7 @@ async def test_coordinator_retry_right_away_on_discovery_already_setup( await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_id = "light.bulb_rgbcw_ddeeff" assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS @@ -219,7 +219,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( ): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS assert config_entry.title == title @@ -235,7 +235,7 @@ async def test_time_sync_startup_and_next_day(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(bulb.async_set_time.mock_calls) == 1 async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 455bad05029..2ed0d34989f 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -292,7 +292,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: assert state.state == "4" await hass.async_block_till_done(wait_background_tasks=True) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -314,7 +314,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(pixels_per_segment=100) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -335,7 +335,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(music_pixels_per_segment=100) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -356,7 +356,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(segments=5) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/folder_watcher/conftest.py b/tests/components/folder_watcher/conftest.py new file mode 100644 index 00000000000..06c0a41d49c --- /dev/null +++ b/tests/components/folder_watcher/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for Folder Watcher integration tests.""" + +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.folder_watcher.async_setup_entry", return_value=True + ): + yield diff --git a/tests/components/folder_watcher/test_config_flow.py b/tests/components/folder_watcher/test_config_flow.py new file mode 100644 index 00000000000..745059717fb --- /dev/null +++ b/tests/components/folder_watcher/test_config_flow.py @@ -0,0 +1,186 @@ +"""Test the Folder Watcher config flow.""" + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.folder_watcher.const import ( + CONF_FOLDER, + CONF_PATTERNS, + DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we get the form.""" + path = tmp_path.as_posix() + hass.config.allowlist_external_dirs = {path} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_not_allowed_path(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we handle not allowed path.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "not_allowed_dir"} + + hass.config.allowlist_external_dirs = {tmp_path} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_not_directory(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we handle not a directory.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: "not_a_directory"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "not_dir"} + + hass.config.allowlist_external_dirs = {path} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_not_readable_dir(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we handle not able to read directory.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("os.access", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "not_readable_dir"} + + hass.config.allowlist_external_dirs = {path} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_already_configured(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we abort when entry is already configured.""" + path = tmp_path.as_posix() + hass.config.allowlist_external_dirs = {path} + + entry = MockConfigEntry( + domain=DOMAIN, + title=f"Folder Watcher {path}", + data={CONF_FOLDER: path}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import(hass: HomeAssistant, tmp_path: Path) -> None: + """Test import flow.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_FOLDER: path, CONF_PATTERNS: ["*"]}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_import_already_configured(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we abort import when entry is already configured.""" + path = tmp_path.as_posix() + + entry = MockConfigEntry( + domain=DOMAIN, + title=f"Folder Watcher {path}", + data={CONF_FOLDER: path}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_FOLDER: path}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index d8beae3b77b..d5461ae71c7 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock import pytest +from homeassistant.components import sensor from homeassistant.components.foobot import sensor as foobot -import homeassistant.components.sensor as sensor from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 015bd809b20..abaad402e1b 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -25,7 +25,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -40,7 +40,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_LATITUDE: 52.42, @@ -67,7 +67,7 @@ async def test_options_flow_invalid_api( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result2 = await hass.config_entries.options.async_configure( @@ -84,7 +84,7 @@ async def test_options_flow_invalid_api( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -100,7 +100,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # With the API key @@ -118,7 +118,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: "SolarForecast150", CONF_DECLINATION: 21, @@ -142,7 +142,7 @@ async def test_options_flow_without_key( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Without the API key @@ -159,7 +159,7 @@ async def test_options_flow_without_key( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: None, CONF_DECLINATION: 21, diff --git a/tests/components/forecast_solar/test_energy.py b/tests/components/forecast_solar/test_energy.py index 246ed866506..1ae3c22c870 100644 --- a/tests/components/forecast_solar/test_energy.py +++ b/tests/components/forecast_solar/test_energy.py @@ -25,7 +25,7 @@ async def test_energy_solar_forecast( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert await energy.async_get_solar_forecast(hass, mock_config_entry.entry_id) == { "wh_hours": { diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index b581888547d..481ec3c0c9d 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -29,7 +29,7 @@ async def test_load_unload_config_entry( mock_config_entry.add_to_hass(hass) await async_setup_component(hass, "forecast_solar", {}) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index a7f0dc3f603..593b527009b 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.forked_daapd.const import ( CONF_LIBRESPOT_JAVA_PORT, @@ -18,6 +17,7 @@ from homeassistant.components.forked_daapd.media_player import async_setup_entry from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import PlatformNotReady from tests.common import MockConfigEntry @@ -63,7 +63,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -86,7 +86,7 @@ async def test_config_flow(hass: HomeAssistant, config_entry) -> None: DOMAIN, context={"source": SOURCE_USER}, data=config_data ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My Music on myhost" assert result["data"][CONF_HOST] == config_data[CONF_HOST] assert result["data"][CONF_PORT] == config_data[CONF_PORT] @@ -99,7 +99,7 @@ async def test_config_flow(hass: HomeAssistant, config_entry) -> None: data=config_entry.data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zeroconf_updates_title(hass: HomeAssistant, config_entry) -> None: @@ -120,7 +120,7 @@ async def test_zeroconf_updates_title(hass: HomeAssistant, config_entry) -> None DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert config_entry.title == "zeroconf_test" assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -136,7 +136,7 @@ async def test_config_flow_no_websocket(hass: HomeAssistant, config_entry) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: @@ -154,7 +154,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( @@ -169,7 +169,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -184,7 +184,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -199,7 +199,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" @@ -221,7 +221,7 @@ async def test_config_flow_zeroconf_valid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_options_flow(hass: HomeAssistant, config_entry) -> None: @@ -237,7 +237,7 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -248,7 +248,7 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: CONF_MAX_PLAYLISTS: 8, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_async_setup_entry_not_ready(hass: HomeAssistant, config_entry) -> None: diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 64ad2b946da..9c0a07aa67c 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -9,9 +9,10 @@ from libpyfoscam.foscam import ( ERROR_FOSCAM_UNKNOWN, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.foscam import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -82,7 +83,7 @@ async def test_user_valid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -103,7 +104,7 @@ async def test_user_valid(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG @@ -116,7 +117,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -134,7 +135,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -144,7 +145,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -162,7 +163,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -172,7 +173,7 @@ async def test_user_invalid_response(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -192,7 +193,7 @@ async def test_user_invalid_response(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_response"} @@ -208,7 +209,7 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -223,7 +224,7 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -233,7 +234,7 @@ async def test_user_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -248,5 +249,5 @@ async def test_user_unknown_exception(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index a7dff79ecfb..ca9e9c12937 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -9,12 +9,12 @@ from freebox_api.exceptions import ( InvalidTokenError, ) -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import MOCK_HOST, MOCK_PORT @@ -46,7 +46,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -55,7 +55,7 @@ async def test_user(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -66,7 +66,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -83,7 +83,7 @@ async def internal_test_link(hass: HomeAssistant) -> None: ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == MOCK_HOST assert result["title"] == MOCK_HOST assert result["data"][CONF_HOST] == MOCK_HOST @@ -112,7 +112,7 @@ async def test_link_bridge_mode_error( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -130,7 +130,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -147,7 +147,7 @@ async def test_on_link_failed(hass: HomeAssistant) -> None: side_effect=AuthorizationError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "register_failed"} with patch( @@ -155,7 +155,7 @@ async def test_on_link_failed(hass: HomeAssistant) -> None: side_effect=HttpRequestError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -163,5 +163,5 @@ async def test_on_link_failed(hass: HomeAssistant) -> None: side_effect=InvalidTokenError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index a0063f72557..0999f157661 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch import pytest -from homeassistant import data_entry_flow from homeassistant.components.freedompro.const import DOMAIN 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 .const import DEVICES @@ -25,7 +25,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -80,6 +80,6 @@ async def test_create_entry(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Freedompro" assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/freedompro/test_init.py b/tests/components/freedompro/test_init.py index 58f9c493582..fd8034818b1 100644 --- a/tests/components/freedompro/test_init.py +++ b/tests/components/freedompro/test_init.py @@ -43,7 +43,7 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry( @@ -53,9 +53,9 @@ async def test_unload_entry( entry = init_integration assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 2e26f67c1eb..acf6b0e98cd 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -74,18 +74,6 @@ class FritzConnectionMock: return self._services[service][action] -class FritzHostMock(FritzHosts): - """FritzHosts mocking.""" - - def get_mesh_topology(self, raw=False): - """Retrurn mocked mesh data.""" - return MOCK_MESH_DATA - - def get_hosts_attributes(self): - """Retrurn mocked host attributes data.""" - return MOCK_HOST_ATTRIBUTES_DATA - - @pytest.fixture(name="fc_data") def fc_data_mock(): """Fixture for default fc_data.""" @@ -107,6 +95,8 @@ def fh_class_mock(): """Fixture that sets up a mocked FritzHosts class.""" with patch( "homeassistant.components.fritz.common.FritzHosts", - new=FritzHostMock, + new=FritzHosts, ) as result: + result.get_mesh_topology = MagicMock(return_value=MOCK_MESH_DATA) + result.get_hosts_attributes = MagicMock(return_value=MOCK_HOST_ATTRIBUTES_DATA) yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 30c9f9be174..0d1222dfcda 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -8,6 +8,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, + CONF_SSL, CONF_USERNAME, ) @@ -22,12 +23,18 @@ MOCK_CONFIG = { CONF_PORT: "1234", CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user", + CONF_SSL: False, } ] } } + MOCK_HOST = "fake_host" -MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} +MOCK_IPS = { + "fritz.box": "192.168.178.1", + "printer": "192.168.178.2", + "server": "192.168.178.3", +} MOCK_MODELNAME = "FRITZ!Box 7530 AX" MOCK_FIRMWARE = "256.07.29" MOCK_FIRMWARE_AVAILABLE = "7.50" @@ -780,6 +787,45 @@ MOCK_MESH_DATA = { ], } +MOCK_NEW_DEVICE_NODE = { + "uid": "n-900", + "device_name": "server", + "device_model": "", + "device_manufacturer": "", + "device_firmware_version": "", + "device_mac_address": "AA:BB:CC:33:44:55", + "is_meshed": False, + "mesh_role": "unknown", + "meshd_version": "0.0", + "node_interfaces": [ + { + "uid": "ni-901", + "name": "eth0", + "type": "LAN", + "mac_address": "AA:BB:CC:33:44:55", + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-902", + "type": "LAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-900", + "node_interface_1_uid": "ni-31", + "node_interface_2_uid": "ni-901", + "max_data_rate_rx": 1000000, + "max_data_rate_tx": 1000000, + "cur_data_rate_rx": 0, + "cur_data_rate_tx": 0, + "cur_availability_rx": 99, + "cur_availability_tx": 99, + } + ], + } + ], +} + MOCK_HOST_ATTRIBUTES_DATA = [ { "Index": 1, @@ -831,9 +877,42 @@ MOCK_HOST_ATTRIBUTES_DATA = [ "X_AVM-DE_FriendlyName": "fritz.box", "X_AVM-DE_FriendlyNameIsWriteable": "0", }, + { + "Index": 3, + "IPAddress": MOCK_IPS["server"], + "MACAddress": "AA:BB:CC:33:44:55", + "Active": True, + "HostName": "server", + "InterfaceType": "Ethernet", + "X_AVM-DE_Port": 1, + "X_AVM-DE_Speed": 1000, + "X_AVM-DE_UpdateAvailable": False, + "X_AVM-DE_UpdateSuccessful": "unknown", + "X_AVM-DE_InfoURL": None, + "X_AVM-DE_MACAddressList": None, + "X_AVM-DE_Model": None, + "X_AVM-DE_URL": f"http://{MOCK_IPS['server']}", + "X_AVM-DE_Guest": False, + "X_AVM-DE_RequestClient": "0", + "X_AVM-DE_VPN": False, + "X_AVM-DE_WANAccess": "granted", + "X_AVM-DE_Disallow": False, + "X_AVM-DE_IsMeshable": "0", + "X_AVM-DE_Priority": "0", + "X_AVM-DE_FriendlyName": "server", + "X_AVM-DE_FriendlyNameIsWriteable": "1", + }, ] MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] +MOCK_USER_INPUT_ADVANCED = MOCK_USER_DATA +MOCK_USER_INPUT_SIMPLE = { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_SSL: False, +} + MOCK_DEVICE_INFO = { ATTR_HOST: MOCK_HOST, ATTR_NEW_SERIAL_NUMBER: MOCK_SERIAL_NUMBER, diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index 106fb7f9bef..14aa46f30a7 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,18 +1,21 @@ """Tests for Fritz!Tools button platform.""" +import copy +from datetime import timedelta from unittest.mock import patch import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.fritz.const import DOMAIN, MeshRoles from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow -from .const import MOCK_USER_DATA +from .const import MOCK_MESH_DATA, MOCK_NEW_DEVICE_NODE, MOCK_USER_DATA -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_button_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: @@ -23,7 +26,7 @@ async def test_button_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED buttons = hass.states.async_all(BUTTON_DOMAIN) assert len(buttons) == 4 @@ -54,7 +57,7 @@ async def test_buttons( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED button = hass.states.get(entity_id) assert button @@ -73,3 +76,113 @@ async def test_buttons( button = hass.states.get(entity_id) assert button.state != STATE_UNKNOWN + + +async def test_wol_button( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test Fritz!Tools wake on LAN button.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + button = hass.states.get("button.printer_wake_on_lan") + assert button + assert button.state == STATE_UNKNOWN + with patch( + "homeassistant.components.fritz.common.AvmWrapper.async_wake_on_lan" + ) as mock_press_action: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.printer_wake_on_lan"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_press_action.assert_called_once_with("AA:BB:CC:00:11:22") + + button = hass.states.get("button.printer_wake_on_lan") + assert button.state != STATE_UNKNOWN + + +async def test_wol_button_new_device( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test WoL button is created for new device at runtime.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + mesh_data = copy.deepcopy(MOCK_MESH_DATA) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + + assert hass.states.get("button.printer_wake_on_lan") + assert not hass.states.get("button.server_wake_on_lan") + + mesh_data["nodes"].append(MOCK_NEW_DEVICE_NODE) + fh_class_mock.get_mesh_topology.return_value = mesh_data + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get("button.printer_wake_on_lan") + assert hass.states.get("button.server_wake_on_lan") + + +async def test_wol_button_absent_for_mesh_slave( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test WoL button not created if interviewed box is in slave mode.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + slave_mesh_data = copy.deepcopy(MOCK_MESH_DATA) + slave_mesh_data["nodes"][0]["mesh_role"] = MeshRoles.SLAVE + fh_class_mock.get_mesh_topology.return_value = slave_mesh_data + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + + button = hass.states.get("button.printer_wake_on_lan") + assert button is None + + +async def test_wol_button_absent_for_non_lan_device( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test WoL button not created if interviewed device is not connected via LAN.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + printer_wifi_data = copy.deepcopy(MOCK_MESH_DATA) + # initialization logic uses the connection type of the `node_interface_1_uid` pair of the printer + # ni-230 is wifi interface of fritzbox + printer_node_interface = printer_wifi_data["nodes"][1]["node_interfaces"][0] + printer_node_interface["type"] = "WLAN" + printer_node_interface["node_links"][0]["node_interface_1_uid"] = "ni-230" + fh_class_mock.get_mesh_topology.return_value = printer_wifi_data + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + + button = hass.states.get("button.printer_wake_on_lan") + assert button is None diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index fbd9886f468..f87fbe722cd 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -10,7 +10,6 @@ from fritzconnection.core.exceptions import ( ) import pytest -from homeassistant import data_entry_flow from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -24,8 +23,19 @@ from homeassistant.components.fritz.const import ( FRITZ_AUTH_EXCEPTIONS, ) from homeassistant.components.ssdp import ATTR_UPNP_UDN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_SSDP, + SOURCE_USER, +) +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -35,12 +45,59 @@ from .const import ( MOCK_REQUEST, MOCK_SSDP_DATA, MOCK_USER_DATA, + MOCK_USER_INPUT_ADVANCED, + MOCK_USER_INPUT_SIMPLE, ) from tests.common import MockConfigEntry -async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> None: +@pytest.mark.parametrize( + ("show_advanced_options", "user_input", "expected_config"), + [ + ( + True, + MOCK_USER_INPUT_ADVANCED, + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 1234, + CONF_SSL: False, + }, + ), + ( + False, + MOCK_USER_INPUT_SIMPLE, + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 49000, + CONF_SSL: False, + }, + ), + ( + False, + {**MOCK_USER_INPUT_SIMPLE, CONF_SSL: True}, + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 49443, + CONF_SSL: True, + }, + ), + ], +) +async def test_user( + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + show_advanced_options: bool, + user_input: dict, + expected_config: dict, +) -> None: """Test starting a flow by user.""" with ( patch( @@ -69,18 +126,20 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={ + "source": SOURCE_USER, + "show_advanced_options": show_advanced_options, + }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_PASSWORD] == "fake_pass" - assert result["data"][CONF_USERNAME] == "fake_user" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == expected_config assert ( result["options"][CONF_CONSIDER_HOME] == DEFAULT_CONSIDER_HOME.total_seconds() @@ -91,12 +150,20 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N assert mock_setup_entry.called +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) async def test_user_already_configured( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + show_advanced_options: bool, + user_input, ) -> None: """Test starting a flow by user with an already configured device.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry(domain=DOMAIN, data=user_input) mock_config.add_to_hass(hass) with ( @@ -125,15 +192,19 @@ async def test_user_already_configured( mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={ + "source": SOURCE_USER, + "show_advanced_options": show_advanced_options, + }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=MOCK_USER_INPUT_SIMPLE ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "already_configured" @@ -142,15 +213,24 @@ async def test_user_already_configured( "error", FRITZ_AUTH_EXCEPTIONS, ) +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) async def test_exception_security( - hass: HomeAssistant, mock_get_source_ip, error + hass: HomeAssistant, + mock_get_source_ip, + error, + show_advanced_options: bool, + user_input, ) -> None: """Test starting a flow by user with invalid credentials.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -158,21 +238,31 @@ async def test_exception_security( side_effect=error, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_AUTH_INVALID -async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> None: +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) +async def test_exception_connection( + hass: HomeAssistant, + mock_get_source_ip, + show_advanced_options: bool, + user_input, +) -> None: """Test starting a flow by user with a connection error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -180,21 +270,28 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> side_effect=FritzConnectionException, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_CANNOT_CONNECT -async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> None: +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) +async def test_exception_unknown( + hass: HomeAssistant, mock_get_source_ip, show_advanced_options: bool, user_input +) -> None: """Test starting a flow by user with an unknown exception.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -202,16 +299,18 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> Non side_effect=OSError, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_UNKNOWN async def test_reauth_successful( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, ) -> None: """Test starting a reauthentication flow.""" @@ -248,7 +347,7 @@ async def test_reauth_successful( data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -259,7 +358,7 @@ async def test_reauth_successful( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_setup_entry.called @@ -274,7 +373,11 @@ async def test_reauth_successful( ], ) async def test_reauth_not_successful( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip, side_effect, error + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + side_effect, + error, ) -> None: """Test starting a reauthentication flow but no connection found.""" @@ -291,7 +394,7 @@ async def test_reauth_not_successful( data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -302,11 +405,181 @@ async def test_reauth_not_successful( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == error +@pytest.mark.parametrize( + ("show_advanced_options", "user_input", "expected_config"), + [ + ( + True, + {CONF_HOST: "host_a", CONF_PORT: 49000, CONF_SSL: False}, + {CONF_HOST: "host_a", CONF_PORT: 49000, CONF_SSL: False}, + ), + ( + True, + {CONF_HOST: "host_a", CONF_PORT: 49443, CONF_SSL: True}, + {CONF_HOST: "host_a", CONF_PORT: 49443, CONF_SSL: True}, + ), + ( + True, + {CONF_HOST: "host_a", CONF_PORT: 12345, CONF_SSL: True}, + {CONF_HOST: "host_a", CONF_PORT: 12345, CONF_SSL: True}, + ), + ( + False, + {CONF_HOST: "host_b", CONF_SSL: False}, + {CONF_HOST: "host_b", CONF_PORT: 49000, CONF_SSL: False}, + ), + ( + False, + {CONF_HOST: "host_b", CONF_SSL: True}, + {CONF_HOST: "host_b", CONF_PORT: 49443, CONF_SSL: True}, + ), + ], +) +async def test_reconfigure_successful( + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + show_advanced_options: bool, + user_input: dict, + expected_config: dict, +) -> None: + """Test starting a reconfigure flow.""" + + 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, + ): + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + "show_advanced_options": show_advanced_options, + }, + data=mock_config.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data == { + **expected_config, + CONF_USERNAME: "fake_user", + CONF_PASSWORD: "fake_pass", + } + + assert mock_setup_entry.called + + +async def test_reconfigure_not_successful( + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, +) -> None: + """Test starting a reconfigure flow but no connection found.""" + + 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=[FritzConnectionException, fc_class_mock], + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch( + "homeassistant.components.fritz.async_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 + mock_request_post.return_value.text = MOCK_REQUEST + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"]["base"] == ERROR_CANNOT_CONNECT + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data == { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 49000, + CONF_SSL: False, + } + + async def test_ssdp_already_configured( hass: HomeAssistant, fc_class_mock, mock_get_source_ip ) -> None: @@ -332,7 +605,7 @@ async def test_ssdp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -361,7 +634,7 @@ async def test_ssdp_already_configured_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -390,7 +663,7 @@ async def test_ssdp_already_configured_host_uuid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -405,7 +678,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) @@ -414,7 +687,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -441,7 +714,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -452,7 +725,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -469,7 +742,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -480,7 +753,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -500,7 +773,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_OLD_DISCOVERY: False, CONF_CONSIDER_HOME: 37, diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index 9dc50cc3378..35d50ff4572 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -28,7 +28,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED entry_dict = entry.as_dict() for key in TO_REDACT: diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index 5d6b9265760..a22ab76fdb6 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -105,7 +105,7 @@ async def test_image_entity( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # test image entity is generated as expected states = hass.states.async_all(IMAGE_DOMAIN) @@ -155,7 +155,7 @@ async def test_image_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED client = await hass_client() resp = await client.get("/api/image_proxy/image.mock_title_guestwifi") @@ -164,7 +164,7 @@ async def test_image_update( fc_class_mock().override_services({**MOCK_FB_SERVICES, **GUEST_WIFI_CHANGED}) 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) resp = await client.get("/api/image_proxy/image.mock_title_guestwifi") resp_body_new = await resp.read() @@ -191,7 +191,7 @@ async def test_image_update_unavailable( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("image.mock_title_guestwifi") assert state diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 0a525192778..be45698e160 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -29,10 +29,10 @@ async def test_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_options_reload( @@ -53,7 +53,7 @@ async def test_options_reload( ) as mock_reload: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() @@ -82,7 +82,7 @@ async def test_setup_auth_fail(hass: HomeAssistant, error) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR @pytest.mark.parametrize( @@ -102,4 +102,4 @@ async def test_setup_fail(hass: HomeAssistant, error) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 37116e66719..f8114238376 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -107,7 +107,7 @@ async def test_sensor_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED sensors = hass.states.async_all(SENSOR_DOMAIN) assert len(sensors) == len(SENSOR_TYPES) diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index 722f16fa0de..b82587d42bd 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -15,6 +15,8 @@ from tests.common import MockConfigEntry MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { "WLANConfiguration1": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -34,9 +36,11 @@ MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, "WLANConfiguration2": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -56,11 +60,13 @@ MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, } MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { "WLANConfiguration1": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -80,9 +86,11 @@ MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, "WLANConfiguration2": { + "GetSSID": {"NewSSID": "WiFi2"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -102,11 +110,13 @@ MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, } MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { "WLANConfiguration1": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -126,9 +136,11 @@ MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, "WLANConfiguration2": { + "GetSSID": {"NewSSID": "WiFi+"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -148,7 +160,7 @@ MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, } @@ -172,15 +184,15 @@ async def test_switch_setup( expected_wifi_names: list[str], fc_class_mock, fh_class_mock, -): +) -> None: """Test setup of Fritz!Tools switches.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + await hass.async_block_till_done(wait_background_tasks=True) + assert entry.state is ConfigEntryState.LOADED switches = hass.states.async_all(Platform.SWITCH) assert len(switches) == 3 diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 97c9cdec25d..c39dd24de02 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -40,7 +40,7 @@ async def test_update_entities_initialized( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED updates = hass.states.async_all(UPDATE_DOMAIN) assert len(updates) == 1 @@ -61,7 +61,7 @@ async def test_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED update = hass.states.get("update.mock_title_fritz_os") assert update is not None @@ -84,7 +84,7 @@ async def test_no_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED update = hass.states.get("update.mock_title_fritz_os") assert update is not None @@ -112,7 +112,7 @@ async def test_available_update_can_be_installed( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED update = hass.states.get("update.mock_title_fritz_os") assert update is not None @@ -124,4 +124,4 @@ async def test_available_update_can_be_installed( {"entity_id": "update.mock_title_fritz_os"}, blocking=True, ) - assert mocked_update_call.assert_called_once + mocked_update_call.assert_called_once() diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 8d366e39f6d..5fb9c853bf5 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -23,9 +23,9 @@ async def setup_config_entry( hass: HomeAssistant, data: dict[str, Any], unique_id: str = "any", - device: Mock = None, - fritz: Mock = None, - template: Mock = None, + device: Mock | None = None, + fritz: Mock | None = None, + template: Mock | None = None, ) -> bool: """Do setup of a MockConfigEntry.""" entry = MockConfigEntry( diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 073a67f22c1..54d222c6899 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -288,6 +288,7 @@ async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock) -> Non async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: """Test setting temperature by mode.""" device = FritzDeviceClimateMock() + device.target_temperature = 0.0 assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -321,9 +322,26 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, fritz: Mock) -> None: assert device.set_target_temperature.call_args_list == [call(0)] +async def test_no_reset_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: + """Test setting hvac mode.""" + device = FritzDeviceClimateMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + True, + ) + assert device.set_target_temperature.call_count == 0 + + async def test_set_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: """Test setting hvac mode.""" device = FritzDeviceClimateMock() + device.target_temperature = 0.0 assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 690082085f8..72d36a8ab63 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -12,7 +12,12 @@ from requests.exceptions import HTTPError from homeassistant.components import ssdp from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -68,13 +73,13 @@ async def test_user(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -89,7 +94,7 @@ async def test_user_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -101,7 +106,7 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -110,13 +115,13 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -130,7 +135,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock) -> None: context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -141,7 +146,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config.data[CONF_USERNAME] == "other_fake_user" assert mock_config.data[CONF_PASSWORD] == "other_fake_password" @@ -159,7 +164,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -170,7 +175,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -187,7 +192,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -198,10 +203,84 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" +async def test_reconfigure_success(hass: HomeAssistant, fritz: Mock) -> None: + """Test starting a reconfigure flow.""" + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "new_host", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + +async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: + """Test starting a reconfigure flow with failure.""" + fritz().login.side_effect = [OSError("Boom"), None] + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "new_host", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"]["base"] == "no_devices_found" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "new_host", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + @pytest.mark.parametrize( ("test_data", "expected_result"), [ @@ -222,7 +301,7 @@ async def test_ssdp( ) assert result["type"] == expected_result - if expected_result == FlowResultType.ABORT: + if expected_result is FlowResultType.ABORT: return assert result["step_id"] == "confirm" @@ -231,7 +310,7 @@ async def test_ssdp( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -247,14 +326,14 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -269,7 +348,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -277,7 +356,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"]["base"] == "invalid_auth" @@ -289,14 +368,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -307,14 +386,14 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -325,13 +404,13 @@ async def test_ssdp_already_in_progress_unique_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +419,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"]) @@ -349,7 +428,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -358,12 +437,12 @@ async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert result["result"].unique_id == "only-a-test" diff --git a/tests/components/fritzbox/test_coordinator.py b/tests/components/fritzbox/test_coordinator.py new file mode 100644 index 00000000000..401fab8f169 --- /dev/null +++ b/tests/components/fritzbox/test_coordinator.py @@ -0,0 +1,111 @@ +"""Tests for the AVM Fritz!Box integration.""" + +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import Mock + +from pyfritzhome import LoginError +from requests.exceptions import ConnectionError, HTTPError + +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_DEVICES +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from . import FritzDeviceCoverMock, FritzDeviceSwitchMock +from .const import MOCK_CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator_update_after_reboot( + hass: HomeAssistant, fritz: Mock +) -> None: + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().update_devices.side_effect = [HTTPError(), ""] + + assert await hass.config_entries.async_setup(entry.entry_id) + assert fritz().update_devices.call_count == 2 + assert fritz().update_templates.call_count == 1 + assert fritz().get_devices.call_count == 1 + assert fritz().get_templates.call_count == 1 + assert fritz().login.call_count == 2 + + +async def test_coordinator_update_after_password_change( + hass: HomeAssistant, fritz: Mock +) -> None: + """Test coordinator after password change.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().update_devices.side_effect = HTTPError() + fritz().login.side_effect = ["", LoginError("some_user")] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert fritz().update_devices.call_count == 1 + assert fritz().get_devices.call_count == 0 + assert fritz().get_templates.call_count == 0 + assert fritz().login.call_count == 2 + + +async def test_coordinator_update_when_unreachable( + hass: HomeAssistant, fritz: Mock +) -> None: + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().update_devices.side_effect = [ConnectionError(), ""] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_coordinator_automatic_registry_cleanup( + hass: HomeAssistant, + fritz: Mock, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test automatic registry cleanup.""" + fritz().get_devices.return_value = [ + FritzDeviceSwitchMock(ain="fake ain switch", name="fake_switch"), + FritzDeviceCoverMock(ain="fake ain cover", name="fake_cover"), + ] + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 11 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 + + fritz().get_devices.return_value = [ + FritzDeviceSwitchMock(ain="fake ain switch", name="fake_switch") + ] + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=35)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 8 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 4ee351f7914..8d7e4249fbd 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, call, patch from pyfritzhome import LoginError import pytest -from requests.exceptions import ConnectionError, HTTPError +from requests.exceptions import ConnectionError as RequestConnectionError from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN @@ -80,6 +80,7 @@ async def test_update_unique_id( new_unique_id: str, ) -> None: """Test unique_id update of integration.""" + fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entry = MockConfigEntry( domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], @@ -138,6 +139,7 @@ async def test_update_unique_id_no_change( unique_id: str, ) -> None: """Test unique_id is not updated of integration.""" + fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entry = MockConfigEntry( domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], @@ -158,62 +160,6 @@ async def test_update_unique_id_no_change( assert entity_migrated.unique_id == unique_id -async def test_coordinator_update_after_reboot( - hass: HomeAssistant, fritz: Mock -) -> None: - """Test coordinator after reboot.""" - entry = MockConfigEntry( - domain=FB_DOMAIN, - data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - unique_id="any", - ) - entry.add_to_hass(hass) - fritz().update_devices.side_effect = [HTTPError(), ""] - - assert await hass.config_entries.async_setup(entry.entry_id) - assert fritz().update_devices.call_count == 2 - assert fritz().update_templates.call_count == 1 - assert fritz().get_devices.call_count == 1 - assert fritz().get_templates.call_count == 1 - assert fritz().login.call_count == 2 - - -async def test_coordinator_update_after_password_change( - hass: HomeAssistant, fritz: Mock -) -> None: - """Test coordinator after password change.""" - entry = MockConfigEntry( - domain=FB_DOMAIN, - data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - unique_id="any", - ) - entry.add_to_hass(hass) - fritz().update_devices.side_effect = HTTPError() - fritz().login.side_effect = ["", LoginError("some_user")] - - assert not await hass.config_entries.async_setup(entry.entry_id) - assert fritz().update_devices.call_count == 1 - assert fritz().get_devices.call_count == 0 - assert fritz().get_templates.call_count == 0 - assert fritz().login.call_count == 2 - - -async def test_coordinator_update_when_unreachable( - hass: HomeAssistant, fritz: Mock -) -> None: - """Test coordinator after reboot.""" - entry = MockConfigEntry( - domain=FB_DOMAIN, - data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - unique_id="any", - ) - entry.add_to_hass(hass) - fritz().update_devices.side_effect = [ConnectionError(), ""] - - assert not await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY - - async def test_unload_remove(hass: HomeAssistant, fritz: Mock) -> None: """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] @@ -325,7 +271,7 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant) -> entry.add_to_hass(hass) with patch( "homeassistant.components.fritzbox.Fritzhome.login", - side_effect=ConnectionError(), + side_effect=RequestConnectionError(), ) as mock_login: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 33e2d8fb125..14f18e84e0c 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -90,7 +90,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -129,7 +129,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_1 assert result["data"] == MOCK_CONFIG_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -172,7 +172,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "phonebook" assert result["errors"] == {} @@ -191,7 +191,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_2 assert result["data"] == { CONF_HOST: MOCK_HOST, @@ -219,7 +219,7 @@ async def test_setup_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == ConnectResult.NO_DEVIES_FOUND @@ -238,7 +238,7 @@ async def test_setup_insufficient_permissions(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == ConnectResult.INSUFFICIENT_PERMISSIONS @@ -260,7 +260,7 @@ async def test_setup_invalid_auth( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.INVALID_AUTH} @@ -282,14 +282,14 @@ async def test_options_flow_correct_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: "+49, 491234"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: ["+49", "491234"]} @@ -311,14 +311,14 @@ async def test_options_flow_incorrect_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: ""} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.MALFORMED_PREFIXES} @@ -340,12 +340,12 @@ async def test_options_flow_no_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: None} diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index c09baeb2d22..bf5ef360752 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -49,7 +49,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -70,7 +70,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Datalogger at 10.9.8.1" assert result2["data"] == { "host": "10.9.8.1", @@ -84,7 +84,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -109,7 +109,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Inverter at 10.9.1.1" assert result2["data"] == { "host": "10.9.1.1", @@ -141,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -168,7 +168,7 @@ async def test_form_no_device(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -189,7 +189,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -217,7 +217,7 @@ async def test_form_already_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -259,7 +259,7 @@ async def test_form_updates_host( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_unload_entry.assert_called_with(hass, entry) @@ -283,13 +283,13 @@ async def test_dhcp(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"SolarNet Datalogger at {MOCK_DHCP_DATA.ip}" assert result["data"] == { "host": MOCK_DHCP_DATA.ip, @@ -314,7 +314,7 @@ async def test_dhcp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -336,5 +336,5 @@ async def test_dhcp_invalid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" diff --git a/tests/components/frontier_silicon/test_config_flow.py b/tests/components/frontier_silicon/test_config_flow.py index 6a5e62f7dce..04bd1febdf8 100644 --- a/tests/components/frontier_silicon/test_config_flow.py +++ b/tests/components/frontier_silicon/test_config_flow.py @@ -52,7 +52,7 @@ async def test_form_default_pin( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -67,7 +67,7 @@ async def test_form_default_pin( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -90,7 +90,7 @@ async def test_form_nondefault_pin( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -104,7 +104,7 @@ async def test_form_nondefault_pin( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] is None @@ -119,7 +119,7 @@ async def test_form_nondefault_pin( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Name of the device" assert result3["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -146,7 +146,7 @@ async def test_form_nondefault_pin_invalid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -160,7 +160,7 @@ async def test_form_nondefault_pin_invalid( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] is None @@ -174,7 +174,7 @@ async def test_form_nondefault_pin_invalid( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result3["errors"] == {"base": result_error} @@ -184,7 +184,7 @@ async def test_form_nondefault_pin_invalid( ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Name of the device" assert result4["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -210,7 +210,7 @@ async def test_invalid_device_url( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -224,7 +224,7 @@ async def test_invalid_device_url( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": result_error} @@ -234,7 +234,7 @@ async def test_invalid_device_url( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Name of the device" assert result3["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -265,7 +265,7 @@ async def test_ssdp( data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -273,7 +273,7 @@ async def test_ssdp( {}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -291,7 +291,7 @@ async def test_ssdp_invalid_location(hass: HomeAssistant) -> None: data=INVALID_MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -308,7 +308,7 @@ async def test_ssdp_already_configured( data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -330,7 +330,7 @@ async def test_ssdp_fail( data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == result_error @@ -347,7 +347,7 @@ async def test_ssdp_nondefault_pin(hass: HomeAssistant) -> None: data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -365,14 +365,14 @@ async def test_reauth_flow(hass: HomeAssistant, config_entry: MockConfigEntry) - }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_config" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "4242"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data[CONF_PIN] == "4242" @@ -404,7 +404,7 @@ async def test_reauth_flow_friendly_name_error( }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_config" with patch( @@ -417,7 +417,7 @@ async def test_reauth_flow_friendly_name_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] == {"base": reason} @@ -425,6 +425,6 @@ async def test_reauth_flow_friendly_name_error( result["flow_id"], user_input={CONF_PIN: "4242"}, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data[CONF_PIN] == "4242" diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py index 9bd4c3a897c..4652ee96047 100644 --- a/tests/components/fully_kiosk/test_button.py +++ b/tests/components/fully_kiosk/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -import homeassistant.components.button as button +from homeassistant.components import button from homeassistant.components.fully_kiosk.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 6a78eda070f..873fb2c6796 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -32,7 +32,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test device" assert result2.get("data") == { CONF_HOST: "1.1.1.1", @@ -94,7 +94,7 @@ async def test_errors( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": reason} @@ -112,7 +112,7 @@ async def test_errors( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test device" assert result3.get("data") == { CONF_HOST: "1.1.1.1", @@ -139,7 +139,7 @@ async def test_duplicate_updates_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -152,7 +152,7 @@ async def test_duplicate_updates_existing_entry( }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "1.1.1.1", @@ -182,7 +182,7 @@ async def test_dhcp_discovery_updates_entry( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.2", @@ -210,7 +210,7 @@ async def test_dhcp_unknown_device( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unknown" @@ -234,7 +234,7 @@ async def test_mqtt_discovery_flow( timestamp=None, ), ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "discovery_confirm" confirmResult = await hass.config_entries.flow.async_configure( @@ -247,7 +247,7 @@ async def test_mqtt_discovery_flow( ) assert confirmResult - assert confirmResult.get("type") == FlowResultType.CREATE_ENTRY + assert confirmResult.get("type") is FlowResultType.CREATE_ENTRY assert confirmResult.get("title") == "Test device" assert confirmResult.get("data") == { CONF_HOST: "192.168.1.234", diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py index b8719a578aa..b6eff4cfa2c 100644 --- a/tests/components/fully_kiosk/test_media_player.py +++ b/tests/components/fully_kiosk/test_media_player.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock, Mock, patch +from homeassistant.components import media_player from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK -import homeassistant.components.media_player as media_player from homeassistant.components.media_source import DOMAIN as MS_DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant diff --git a/tests/components/fully_kiosk/test_number.py b/tests/components/fully_kiosk/test_number.py index b4ac50cb076..2fbbf751725 100644 --- a/tests/components/fully_kiosk/test_number.py +++ b/tests/components/fully_kiosk/test_number.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock +from homeassistant.components import number from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL -import homeassistant.components.number as number from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 03ac00ef677..5b3b5e651b0 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock +from homeassistant.components import switch from homeassistant.components.fully_kiosk.const import DOMAIN -import homeassistant.components.switch as switch from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index e35012a02e8..9250c26926a 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -5,6 +5,9 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.fyta.const import CONF_EXPIRATION +from homeassistant.const import CONF_ACCESS_TOKEN + from .test_config_flow import ACCESS_TOKEN, EXPIRATION @@ -18,8 +21,24 @@ def mock_fyta(): return_value=mock_fyta_api, ) as mock_fyta_api: mock_fyta_api.return_value.login.return_value = { - "access_token": ACCESS_TOKEN, - "expiration": EXPIRATION, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: EXPIRATION, + } + yield mock_fyta_api + + +@pytest.fixture +def mock_fyta_init(): + """Build a fixture for the Fyta API that connects successfully and returns one device.""" + + mock_fyta_api = AsyncMock() + with patch( + "homeassistant.components.fyta.FytaConnector", + return_value=mock_fyta_api, + ) as mock_fyta_api: + mock_fyta_api.return_value.login.return_value = { + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: EXPIRATION, } yield mock_fyta_api diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index b21be5abb90..69478d04ca0 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -1,6 +1,6 @@ """Test the fyta config flow.""" -from datetime import datetime +from datetime import UTC, datetime from unittest.mock import AsyncMock from fyta_cli.fyta_exceptions import ( @@ -10,28 +10,29 @@ from fyta_cli.fyta_exceptions import ( ) 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 import config_entries +from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry USERNAME = "fyta_user" PASSWORD = "fyta_pass" ACCESS_TOKEN = "123xyz" -EXPIRATION = datetime.now() +EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC) async def test_user_flow( - hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry + hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry: AsyncMock ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -39,9 +40,14 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME - assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + assert result2["data"] == { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: "2024-12-31T10:00:00+00:00", + } assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +65,7 @@ async def test_form_exceptions( exception: Exception, error: dict[str, str], mock_fyta: AsyncMock, - mock_setup_entry, + mock_setup_entry: AsyncMock, ) -> None: """Test we can handle Form exceptions.""" @@ -75,7 +81,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == error @@ -87,10 +93,12 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert result["data"][CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" assert len(mock_setup_entry.mock_calls) == 1 @@ -108,7 +116,7 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -118,5 +126,73 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> Non ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@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_reauth( + hass: HomeAssistant, + exception: Exception, + error: dict[str, str], + mock_fyta: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauth-flow works.""" + + entry = MockConfigEntry( + domain=DOMAIN, + title=USERNAME, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: "2024-06-30T10:00:00+00:00", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + 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"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + 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: "other_username", CONF_PASSWORD: "other_password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_USERNAME] == "other_username" + assert entry.data[CONF_PASSWORD] == "other_password" + assert entry.data[CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert entry.data[CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" diff --git a/tests/components/fyta/test_init.py b/tests/components/fyta/test_init.py new file mode 100644 index 00000000000..844a818df85 --- /dev/null +++ b/tests/components/fyta/test_init.py @@ -0,0 +1,42 @@ +"""Test the initialization.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .test_config_flow import ACCESS_TOKEN, PASSWORD, USERNAME + +from tests.common import MockConfigEntry + + +async def test_migrate_config_entry( + hass: HomeAssistant, + mock_fyta_init: AsyncMock, +) -> None: + """Test successful migration of entry data.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=USERNAME, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + assert entry.version == 1 + assert entry.minor_version == 1 + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data[CONF_USERNAME] == USERNAME + assert entry.data[CONF_PASSWORD] == PASSWORD + assert entry.data[CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert entry.data[CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index ae735d71e55..729d31e413c 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM with patch( "homeassistant.components.garages_amsterdam.async_setup_entry", @@ -30,7 +30,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "IJDok" assert "result" in result2 assert result2["result"].unique_id == "IJDok" @@ -59,5 +59,5 @@ async def test_error_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == reason diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index af882e35751..052de4bf311 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -88,11 +88,11 @@ def mock_client( val = mock_read_char_raw[uuid] if isinstance(val, Exception): raise val - return val except KeyError: if default is SENTINEL: raise CharacteristicNotFound from KeyError return default + return val def _all_char(): return set(mock_read_char_raw.keys()) diff --git a/tests/components/gardena_bluetooth/test_config_flow.py b/tests/components/gardena_bluetooth/test_config_flow.py index 7707a13180f..3b4e9c242b3 100644 --- a/tests/components/gardena_bluetooth/test_config_flow.py +++ b/tests/components/gardena_bluetooth/test_config_flow.py @@ -103,7 +103,7 @@ async def test_bluetooth( # Inject the service info will trigger the flow to start inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) result = next(iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN))) diff --git a/tests/components/gardena_bluetooth/test_init.py b/tests/components/gardena_bluetooth/test_init.py index 1f294c6169d..53688846c07 100644 --- a/tests/components/gardena_bluetooth/test_init.py +++ b/tests/components/gardena_bluetooth/test_init.py @@ -57,6 +57,6 @@ async def test_setup_retry( mock_client.read_char.side_effect = original_read_char 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) assert mock_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 71e5dfdb5d5..f11848162cd 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN from homeassistant.const import ( CONF_LATITUDE, @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(name="gdacs_setup", autouse=True) @@ -30,7 +31,7 @@ async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -39,7 +40,7 @@ async def test_show_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -52,7 +53,7 @@ async def test_step_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index e9233ffc559..92a9298cbd5 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -68,11 +68,10 @@ def mock_create_stream(): mock_stream.add_provider.return_value = mock_provider mock_stream.start = AsyncMock() mock_stream.stop = AsyncMock() - fake_create_stream = patch( + return patch( "homeassistant.components.generic.config_flow.create_stream", return_value=mock_stream, ) - return fake_create_stream @pytest.fixture diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index d9b3c848eb6..841fb710717 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -27,6 +27,7 @@ from homeassistant.components.stream import ( CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) from homeassistant.components.stream.worker import StreamWorkerError +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -84,7 +85,7 @@ async def test_form( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" client = await hass_client() preview_id = result1["flow_id"] @@ -97,7 +98,7 @@ async def test_form( user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -127,7 +128,7 @@ async def test_form_only_stillimage( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} data = TESTDATA.copy() @@ -138,13 +139,13 @@ async def test_form_only_stillimage( data, ) await hass.async_block_till_done() - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -171,13 +172,13 @@ async def test_form_reject_still_preview( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: False}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" @@ -201,7 +202,7 @@ async def test_form_still_preview_cam_off( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" preview_id = result1["flow_id"] # Try to view the image, should be unavailable. @@ -222,14 +223,14 @@ async def test_form_only_stillimage_gif( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -247,14 +248,14 @@ async def test_form_only_svg_whitespace( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @respx.mock @@ -282,14 +283,14 @@ async def test_form_only_still_sample( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @respx.mock @@ -371,13 +372,13 @@ async def test_form_rtsp_mode( result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -408,14 +409,14 @@ async def test_form_only_stream( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result3 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -451,7 +452,7 @@ async def test_form_still_and_stream_not_provided( CONF_VERIFY_SSL: False, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_still_image_or_stream_url"} @@ -501,7 +502,7 @@ async def test_form_image_http_exceptions( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == expected_message @@ -518,7 +519,7 @@ async def test_form_stream_invalidimage( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"still_image_url": "invalid_still_image"} @@ -535,7 +536,7 @@ async def test_form_stream_invalidimage2( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"still_image_url": "unable_still_load_no_image"} @@ -552,7 +553,7 @@ async def test_form_stream_invalidimage3( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"still_image_url": "invalid_still_image"} @@ -571,7 +572,7 @@ async def test_form_stream_timeout(hass: HomeAssistant, fakeimg_png, user_flow) user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "timeout"} @@ -588,7 +589,7 @@ async def test_form_stream_worker_error( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "Some message"} @@ -606,7 +607,7 @@ async def test_form_stream_permission_error( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "stream_not_permitted"} @@ -623,7 +624,7 @@ async def test_form_no_route_to_host( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "stream_no_route_to_host"} @@ -640,7 +641,7 @@ async def test_form_stream_io_error( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "stream_io_error"} @@ -680,7 +681,7 @@ async def test_options_template_error( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # try updating the still image url @@ -691,16 +692,16 @@ async def test_options_template_error( result["flow_id"], user_input=data, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "confirm_still" result2a = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: True} ) - assert result2a["type"] == FlowResultType.CREATE_ENTRY + assert result2a["type"] is FlowResultType.CREATE_ENTRY result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "init" # verify that an invalid template reports the correct UI error. @@ -709,7 +710,7 @@ async def test_options_template_error( result3["flow_id"], user_input=data, ) - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4["errors"] == {"still_image_url": "template_error"} # verify that an invalid template reports the correct UI error. @@ -720,7 +721,7 @@ async def test_options_template_error( user_input=data, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5["errors"] == {"stream_source": "template_error"} # verify that an relative stream url is rejected. @@ -730,7 +731,7 @@ async def test_options_template_error( result5["flow_id"], user_input=data, ) - assert result6.get("type") == FlowResultType.FORM + assert result6.get("type") is FlowResultType.FORM assert result6["errors"] == {"stream_source": "relative_url"} # verify that an malformed stream url is rejected. @@ -740,7 +741,7 @@ async def test_options_template_error( result6["flow_id"], user_input=data, ) - assert result7.get("type") == FlowResultType.FORM + assert result7.get("type") is FlowResultType.FORM assert result7["errors"] == {"stream_source": "malformed_url"} @@ -778,7 +779,7 @@ async def test_options_only_stream( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # try updating the config options @@ -787,13 +788,13 @@ async def test_options_only_stream( result["flow_id"], user_input=data, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "confirm_still" result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: True} ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" @@ -804,11 +805,11 @@ async def test_unload_entry(hass: HomeAssistant, fakeimg_png) -> None: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED async def test_reload_on_title_change(hass: HomeAssistant) -> None: @@ -823,7 +824,7 @@ async def test_reload_on_title_change(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED assert hass.states.get("camera.my_title").attributes["friendly_name"] == "My Title" hass.config_entries.async_update_entry(mock_entry, title="New Title") @@ -886,7 +887,7 @@ async def test_use_wallclock_as_timestamps_option( result = await hass.config_entries.options.async_init( mock_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with ( patch("homeassistant.components.generic.async_setup_entry", return_value=True), @@ -896,12 +897,12 @@ async def test_use_wallclock_as_timestamps_option( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM # Test what happens if user rejects the preview result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: False} ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "init" with ( patch("homeassistant.components.generic.async_setup_entry", return_value=True), @@ -911,10 +912,10 @@ async def test_use_wallclock_as_timestamps_option( result3["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "confirm_still" result5 = await hass.config_entries.options.async_configure( result4["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index fdad20f5b2d..ef7a2c90aa9 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -40,7 +40,9 @@ from tests.common import ( assert_setup_component, async_fire_time_changed, mock_restore_cache, + setup_test_component_platform, ) +from tests.components.switch.common import MockSwitch ENTITY = "humidifier.test" ENT_SENSOR = "sensor.test" @@ -127,12 +129,11 @@ async def test_humidifier_input_boolean(hass: HomeAssistant, setup_comp_1) -> No async def test_humidifier_switch( - hass: HomeAssistant, setup_comp_1, enable_custom_integrations: None + hass: HomeAssistant, setup_comp_1, mock_switch_entities: list[MockSwitch] ) -> None: """Test humidifier switching test switch.""" - platform = getattr(hass.components, "test.switch") - platform.init() - switch_1 = platform.ENTITIES[1] + setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) + switch_1 = mock_switch_entities[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index fdcad219d93..ff409511221 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -55,8 +55,10 @@ from tests.common import ( async_mock_service, get_fixture_path, mock_restore_cache, + setup_test_component_platform, ) from tests.components.climate import common +from tests.components.switch.common import MockSwitch ENTITY = "climate.test" ENT_SENSOR = "sensor.test" @@ -140,12 +142,11 @@ async def test_heater_input_boolean(hass: HomeAssistant, setup_comp_1) -> None: async def test_heater_switch( - hass: HomeAssistant, setup_comp_1, enable_custom_integrations: None + hass: HomeAssistant, setup_comp_1, mock_switch_entities: list[MockSwitch] ) -> None: """Test heater switching test switch.""" - platform = getattr(hass.components, "test.switch") - platform.init() - switch_1 = platform.ENTITIES[1] + setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) + switch_1 = mock_switch_entities[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) @@ -807,10 +808,10 @@ async def test_heating_cooling_switch_does_not_toggle_when_within_min_cycle_dura # 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) + _setup_sensor(hass, sensor_temperature) await hass.async_block_till_done() # Then @@ -848,10 +849,10 @@ async def test_heating_cooling_switch_toggles_when_outside_min_cycle_duration( fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with freeze_time(fake_changed): calls = _setup_switch(hass, initial_switch_state) - _setup_sensor(hass, sensor_temperature) # When await common.async_set_temperature(hass, target_temperature) + _setup_sensor(hass, sensor_temperature) await hass.async_block_till_done() # Then @@ -893,10 +894,10 @@ async def test_hvac_mode_change_toggles_heating_cooling_switch_even_when_within_ # 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) + _setup_sensor(hass, sensor_temperature) await hass.async_block_till_done() # Then diff --git a/tests/components/geo_json_events/test_config_flow.py b/tests/components/geo_json_events/test_config_flow.py index a6e20ad4ba8..fe21bccc7aa 100644 --- a/tests/components/geo_json_events/test_config_flow.py +++ b/tests/components/geo_json_events/test_config_flow.py @@ -31,7 +31,7 @@ async def test_duplicate_error_user( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,7 +44,7 @@ async def test_duplicate_error_user( }, }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -54,7 +54,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -67,7 +67,7 @@ async def test_step_user(hass: HomeAssistant) -> None: }, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "http://geo.json.local/geo_json_events.json (-41.2, 174.7)" ) diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index 85461d60aac..b8045ad495c 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -72,16 +72,13 @@ async def test_if_fires_on_zone_enter(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" + " - {{ trigger.id }}" ) }, }, @@ -285,15 +282,12 @@ async def test_if_fires_on_zone_appear(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" ) }, }, @@ -334,15 +328,12 @@ async def test_if_fires_on_zone_appear_2(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" ) }, }, @@ -399,15 +390,12 @@ async def test_if_fires_on_zone_disappear(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" ) }, }, diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 15f7ee0972f..f4e8f0c8a96 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -61,7 +61,7 @@ async def test_full_flow( }, ) - assert result.get("type") == FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP assert result.get("step_id") == "auth" assert result.get("url") == ( f"{CURRENT_ENVIRONMENT_URLS['authorize_url']}?response_type=code&client_id={CLIENT_ID}" @@ -156,7 +156,7 @@ async def test_oauth_error( "redirect_uri": REDIRECT_URI, }, ) - assert result.get("type") == FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -176,7 +176,7 @@ async def test_oauth_error( ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "oauth_error" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index d5d77c1387a..389a4647e2e 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zone from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN from homeassistant.config import async_process_ha_core_config @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_NOT_HOME, ) 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 homeassistant.setup import async_setup_component from homeassistant.util import slugify @@ -157,10 +158,10 @@ async def webhook_id(hass, geofency_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index d4b406cf054..61729776f9c 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.geonetnz_quakes import ( CONF_MINIMUM_MAGNITUDE, CONF_MMI, @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: @@ -27,7 +28,7 @@ async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -36,7 +37,7 @@ async def test_show_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -64,7 +65,7 @@ async def test_step_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -95,7 +96,7 @@ async def test_step_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index e314896dd6b..b074bdffa20 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -3,7 +3,6 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.geonetnz_volcano import config_flow from homeassistant.const import ( CONF_LATITUDE, @@ -13,6 +12,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: @@ -34,7 +34,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -61,7 +61,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ), ): result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -91,7 +91,7 @@ async def test_step_user(hass: HomeAssistant) -> None: ), ): result = await flow.async_step_user(user_input=conf) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/gios/snapshots/test_sensor.ambr b/tests/components/gios/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..c67cc3e4d7c --- /dev/null +++ b/tests/components/gios/snapshots/test_sensor.ambr @@ -0,0 +1,774 @@ +# serializer version: 1 +# name: test_sensor[sensor.home_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air quality index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'aqi', + 'unique_id': '123-aqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_benzene-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.home_benzene', + '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': None, + 'original_icon': None, + 'original_name': 'Benzene', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'c6h6', + 'unique_id': '123-c6h6', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_benzene-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'friendly_name': 'Home Benzene', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_benzene', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.23789', + }) +# --- +# name: test_sensor[sensor.home_carbon_monoxide-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.home_carbon_monoxide', + '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': None, + 'original_icon': None, + 'original_name': 'Carbon monoxide', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co', + 'unique_id': '123-co', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_carbon_monoxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'friendly_name': 'Home Carbon monoxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_carbon_monoxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '251.874', + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide-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.home_nitrogen_dioxide', + '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': 'Nitrogen dioxide', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-no2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'nitrogen_dioxide', + 'friendly_name': 'Home Nitrogen dioxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_nitrogen_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.13411', + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_nitrogen_dioxide_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Nitrogen dioxide index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'no2_index', + 'unique_id': '123-no2-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Nitrogen dioxide index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_nitrogen_dioxide_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_ozone-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.home_ozone', + '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': 'Ozone', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-o3', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_ozone-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'ozone', + 'friendly_name': 'Home Ozone', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ozone', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95.7768', + }) +# --- +# name: test_sensor[sensor.home_ozone_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_ozone_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Ozone index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'o3_index', + 'unique_id': '123-o3-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_ozone_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Ozone index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_ozone_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_pm10-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.home_pm10', + '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': 'PM10', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-pm10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'pm10', + 'friendly_name': 'Home PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.8344', + }) +# --- +# name: test_sensor[sensor.home_pm10_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_pm10_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10 index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pm10_index', + 'unique_id': '123-pm10-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_pm10_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home PM10 index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_pm10_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_pm2_5-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.home_pm2_5', + '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': 'PM2.5', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-pm25', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'pm25', + 'friendly_name': 'Home PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensor[sensor.home_pm2_5_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_pm2_5_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5 index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pm25_index', + 'unique_id': '123-pm25-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_pm2_5_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home PM2.5 index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_pm2_5_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide-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.home_sulphur_dioxide', + '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': 'Sulphur dioxide', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-so2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'sulphur_dioxide', + 'friendly_name': 'Home Sulphur dioxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_sulphur_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.35478', + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_sulphur_dioxide_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sulphur dioxide index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'so2_index', + 'unique_id': '123-so2-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Sulphur dioxide index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_sulphur_dioxide_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'very_good', + }) +# --- diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 4471cfa64ec..a96b065574a 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from gios import ApiError -from homeassistant import data_entry_flow from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import STATIONS @@ -28,7 +28,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -112,7 +112,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=CONFIG) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Name 1" assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 60e8722ba24..b24d88ccb8d 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -6,249 +6,28 @@ import json from unittest.mock import patch from gios import ApiError +from syrupy import SnapshotAssertion -from homeassistant.components.gios.const import ATTRIBUTION, DOMAIN -from homeassistant.components.sensor import ( - ATTR_OPTIONS, - ATTR_STATE_CLASS, - DOMAIN as PLATFORM, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - STATE_UNAVAILABLE, -) +from homeassistant.components.gios.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as PLATFORM +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from . import init_integration -from tests.common import async_fire_time_changed, load_fixture +from tests.common import async_fire_time_changed, load_fixture, snapshot_platform -async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_sensor( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the sensor.""" - await init_integration(hass) + with patch("homeassistant.components.gios.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - state = hass.states.get("sensor.home_benzene") - assert state - assert state.state == "0.23789" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get("sensor.home_benzene") - assert entry - assert entry.unique_id == "123-c6h6" - - state = hass.states.get("sensor.home_carbon_monoxide") - assert state - assert state.state == "251.874" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_carbon_monoxide") - assert entry - assert entry.unique_id == "123-co" - - state = hass.states.get("sensor.home_nitrogen_dioxide") - assert state - assert state.state == "7.13411" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.NITROGEN_DIOXIDE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_nitrogen_dioxide") - assert entry - assert entry.unique_id == "123-no2" - - state = hass.states.get("sensor.home_nitrogen_dioxide_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_nitrogen_dioxide_index") - assert entry - assert entry.unique_id == "123-no2-index" - - state = hass.states.get("sensor.home_ozone") - assert state - assert state.state == "95.7768" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.OZONE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_ozone") - assert entry - assert entry.unique_id == "123-o3" - - state = hass.states.get("sensor.home_ozone_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_ozone_index") - assert entry - assert entry.unique_id == "123-o3-index" - - state = hass.states.get("sensor.home_pm10") - assert state - assert state.state == "16.8344" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_pm10") - assert entry - assert entry.unique_id == "123-pm10" - - state = hass.states.get("sensor.home_pm10_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_pm10_index") - assert entry - assert entry.unique_id == "123-pm10-index" - - state = hass.states.get("sensor.home_pm2_5") - assert state - assert state.state == "4" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_pm2_5") - assert entry - assert entry.unique_id == "123-pm25" - - state = hass.states.get("sensor.home_pm2_5_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_pm2_5_index") - assert entry - assert entry.unique_id == "123-pm25-index" - - state = hass.states.get("sensor.home_sulphur_dioxide") - assert state - assert state.state == "4.35478" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SULPHUR_DIOXIDE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_sulphur_dioxide") - assert entry - assert entry.unique_id == "123-so2" - - state = hass.states.get("sensor.home_sulphur_dioxide_index") - assert state - assert state.state == "very_good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_sulphur_dioxide_index") - assert entry - assert entry.unique_id == "123-so2-index" - - state = hass.states.get("sensor.home_air_quality_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_air_quality_index") - assert entry - assert entry.unique_id == "123-aqi" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: diff --git a/tests/components/github/common.py b/tests/components/github/common.py index d850ce1bba8..5007496c9fe 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -4,8 +4,8 @@ from __future__ import annotations import json -from homeassistant import config_entries from homeassistant.components.github.const import CONF_REPOSITORIES, DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -50,4 +50,4 @@ async def setup_github_integration( await hass.async_block_till_done() assert setup_result - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index c715889b7dc..a721298c129 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -60,7 +60,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "device" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS # Wait for the task to start before configuring await hass.async_block_till_done() @@ -74,7 +74,7 @@ async def test_full_user_flow_implementation( ) assert result["title"] == "" - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_ACCESS_TOKEN] == MOCK_ACCESS_TOKEN assert "options" in result @@ -94,7 +94,7 @@ async def test_flow_with_registration_failure( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result.get("reason") == "could_not_register" @@ -123,11 +123,11 @@ async def test_flow_with_activation_failure( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "could_not_register" @@ -157,7 +157,7 @@ async def test_flow_with_remove_while_activating( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert hass.config_entries.flow.async_get(result["flow_id"]) @@ -181,7 +181,7 @@ async def test_already_configured( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -276,7 +276,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index f0f1fe01796..beba7163bc2 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -1,5 +1,6 @@ """Tests for Glances.""" +from datetime import datetime from typing import Any MOCK_USER_INPUT: dict[str, Any] = { @@ -11,173 +12,17 @@ MOCK_USER_INPUT: dict[str, Any] = { "verify_ssl": True, } -MOCK_DATA = { - "cpu": { - "total": 10.6, - "user": 7.6, - "system": 2.1, - "idle": 88.8, - "nice": 0.0, - "iowait": 0.6, - }, - "diskio": [ - { - "time_since_update": 1, - "disk_name": "nvme0n1", - "read_count": 12, - "write_count": 466, - "read_bytes": 184320, - "write_bytes": 23863296, - "key": "disk_name", - }, - ], - "docker": { - "containers": [ - { - "key": "name", - "name": "container1", - "Status": "running", - "cpu": {"total": 50.94973493230174}, - "cpu_percent": 50.94973493230174, - "memory": { - "usage": 1120321536, - "limit": 3976318976, - "rss": 480641024, - "cache": 580915200, - "max_usage": 1309597696, - }, - "memory_usage": 539406336, - }, - { - "key": "name", - "name": "container2", - "Status": "running", - "cpu": {"total": 26.23567931034483}, - "cpu_percent": 26.23567931034483, - "memory": { - "usage": 85139456, - "limit": 3976318976, - "rss": 33677312, - "cache": 35012608, - "max_usage": 87650304, - }, - "memory_usage": 50126848, - }, - ] - }, - "fs": [ - { - "device_name": "/dev/sda8", - "fs_type": "ext4", - "mnt_point": "/ssl", - "size": 511320748032, - "used": 32910458880, - "free": 457917374464, - "percent": 6.7, - "key": "mnt_point", - }, - { - "device_name": "/dev/sda8", - "fs_type": "ext4", - "mnt_point": "/media", - "size": 511320748032, - "used": 32910458880, - "free": 457917374464, - "percent": 6.7, - "key": "mnt_point", - }, - ], - "mem": { - "total": 3976318976, - "available": 2878337024, - "percent": 27.6, - "used": 1097981952, - "free": 2878337024, - "active": 567971840, - "inactive": 1679704064, - "buffers": 149807104, - "cached": 1334816768, - "shared": 1499136, - }, - "sensors": [ - { - "label": "cpu_thermal 1", - "value": 59, - "warning": None, - "critical": None, - "unit": "C", - "type": "temperature_core", - "key": "label", - }, - { - "label": "err_temp", - "value": "ERR", - "warning": None, - "critical": None, - "unit": "C", - "type": "temperature_hdd", - "key": "label", - }, - { - "label": "na_temp", - "value": "NA", - "warning": None, - "critical": None, - "unit": "C", - "type": "temperature_hdd", - "key": "label", - }, - ], - "system": { - "os_name": "Linux", - "hostname": "fedora-35", - "platform": "64bit", - "linux_distro": "Fedora Linux 35", - "os_version": "5.15.6-200.fc35.x86_64", - "hr_name": "Fedora Linux 35 64bit", - }, - "raid": { - "md3": { - "status": "active", - "type": "raid1", - "components": {"sdh1": "2", "sdi1": "0"}, - "available": "2", - "used": "2", - "config": "UU", - }, - "md1": { - "status": "active", - "type": "raid1", - "components": {"sdg": "0", "sde": "1"}, - "available": "2", - "used": "2", - "config": "UU", - }, - "md4": { - "status": "active", - "type": "raid1", - "components": {"sdf1": "1", "sdb1": "0"}, - "available": "2", - "used": "2", - "config": "UU", - }, - "md0": { - "status": "active", - "type": "raid1", - "components": {"sdc": "2", "sdd": "3"}, - "available": "2", - "used": "2", - "config": "UU", - }, - }, - "uptime": "3 days, 10:25:20", -} +MOCK_REFERENCE_DATE: datetime = datetime.fromisoformat("2024-02-13T14:13:12") HA_SENSOR_DATA: dict[str, Any] = { "fs": { "/ssl": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5}, "/media": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5}, }, + "diskio": { + "nvme0n1": {"read": 184320, "write": 23863296}, + "sda": {"read": 3859, "write": 25954}, + }, "sensors": { "cpu_thermal 1": {"temperature_core": 59}, "err_temp": {"temperature_hdd": "unavailable"}, @@ -207,4 +52,18 @@ HA_SENSOR_DATA: dict[str, Any] = { "config": "UU", }, }, + "network": { + "lo": {"is_up": True, "rx": 7646, "tx": 7646, "speed": 0.0}, + "dummy0": {"is_up": False, "rx": 0.0, "tx": 0.0, "speed": 0.0}, + "eth0": {"is_up": True, "rx": 3953, "tx": 5995, "speed": 9.8}, + }, + "uptime": "3 days, 10:25:20", + "gpu": { + "NVIDIA GeForce RTX 3080 (GPU 0)": { + "temperature": 51, + "mem": 8.41064453125, + "proc": 26, + "fan_speed": 0, + } + }, } diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index 23242f66071..662e95c6a1c 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -200,6 +200,114 @@ 'state': '59', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_rx-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.0_0_0_0_dummy0_rx', + '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': None, + 'original_name': 'dummy0 RX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rx', + 'unique_id': 'test-dummy0-rx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 dummy0 RX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_dummy0_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.000000', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_tx-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.0_0_0_0_dummy0_tx', + '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': None, + 'original_name': 'dummy0 TX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_tx', + 'unique_id': 'test-dummy0-tx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 dummy0 TX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_dummy0_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.000000', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_err_temp_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -251,6 +359,222 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_rx-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.0_0_0_0_eth0_rx', + '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': None, + 'original_name': 'eth0 RX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rx', + 'unique_id': 'test-eth0-rx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 eth0 RX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_eth0_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.03162', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_tx-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.0_0_0_0_eth0_tx', + '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': None, + 'original_name': 'eth0 TX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_tx', + 'unique_id': 'test-eth0-tx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 eth0 TX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_eth0_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.04796', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_rx-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.0_0_0_0_lo_rx', + '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': None, + 'original_name': 'lo RX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rx', + 'unique_id': 'test-lo-rx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 lo RX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_lo_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06117', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_tx-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.0_0_0_0_lo_tx', + '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': None, + 'original_name': 'lo TX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_tx', + 'unique_id': 'test-lo-tx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 lo TX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_lo_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06117', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_md1_available-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -802,6 +1126,426 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_speed-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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_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': 'NVIDIA GeForce RTX 3080 (GPU 0) fan speed', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_speed', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-fan_speed', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) fan speed', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_usage-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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_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': 'NVIDIA GeForce RTX 3080 (GPU 0) memory usage', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gpu_memory_usage', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-mem', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) memory usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.41064453125', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage-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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NVIDIA GeForce RTX 3080 (GPU 0) processor usage', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gpu_processor_usage', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-proc', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) processor usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_temperature-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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_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': 'NVIDIA GeForce RTX 3080 (GPU 0) temperature', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_read-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.0_0_0_0_nvme0n1_disk_read', + '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': None, + 'original_name': 'nvme0n1 disk read', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_read', + 'unique_id': 'test-nvme0n1-read', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_read-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 nvme0n1 disk read', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvme0n1_disk_read', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.184320', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_write-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.0_0_0_0_nvme0n1_disk_write', + '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': None, + 'original_name': 'nvme0n1 disk write', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_write', + 'unique_id': 'test-nvme0n1-write', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_write-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 nvme0n1 disk write', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvme0n1_disk_write', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.863296', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_read-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.0_0_0_0_sda_disk_read', + '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': None, + 'original_name': 'sda disk read', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_read', + 'unique_id': 'test-sda-read', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_read-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 sda disk read', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_sda_disk_read', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.003859', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_write-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.0_0_0_0_sda_disk_write', + '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': None, + 'original_name': 'sda disk write', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_write', + 'unique_id': 'test-sda-write', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_write-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 sda disk write', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_sda_disk_write', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.025954', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_ssl_disk_free-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -954,3 +1698,50 @@ 'state': '30.7', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_uptime-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.0_0_0_0_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uptime', + 'unique_id': 'test--uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': '0.0.0.0 Uptime', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-02-10T03:47:52+00:00', + }) +# --- diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 09dc638bb53..a7d6934e32d 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -32,14 +32,14 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "0.0.0.0:61208" assert result["data"] == MOCK_USER_INPUT @@ -65,7 +65,7 @@ async def test_form_fails( result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": message} @@ -80,7 +80,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -98,7 +98,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "username"} @@ -109,7 +109,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -137,7 +137,7 @@ async def test_reauth_fails( data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "username"} @@ -148,7 +148,7 @@ async def test_reauth_fails( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": message} result3 = await hass.config_entries.flow.async_configure( @@ -158,5 +158,5 @@ async def test_reauth_fails( }, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/glances/test_init.py b/tests/components/glances/test_init.py index aa861dc5518..02fa6960c2f 100644 --- a/tests/components/glances/test_init.py +++ b/tests/components/glances/test_init.py @@ -27,7 +27,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_entry_deprecated_version( @@ -45,7 +45,7 @@ async def test_entry_deprecated_version( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED issue = issue_registry.async_get_issue(DOMAIN, "deprecated_version") assert issue is not None diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index ebe8b75b618..7dee47680ed 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -1,25 +1,36 @@ """Tests for glances sensors.""" +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_USER_INPUT +from . import HA_SENSOR_DATA, MOCK_REFERENCE_DATE, MOCK_USER_INPUT -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_sensor_states( - hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test sensor states are correctly collected from library.""" + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) assert entity_entries @@ -28,3 +39,35 @@ async def test_sensor_states( assert hass.states.get(entity_entry.entity_id) == snapshot( name=f"{entity_entry.entity_id}-state" ) + + +async def test_uptime_variation( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_api: AsyncMock +) -> None: + """Test uptime small variation update.""" + + # Init with reference time + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + uptime_state = hass.states.get("sensor.0_0_0_0_uptime").state + + # Time change should not change uptime (absolute date) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + uptime_state2 = hass.states.get("sensor.0_0_0_0_uptime").state + assert uptime_state2 == uptime_state + + mock_data = HA_SENSOR_DATA.copy() + mock_data["uptime"] = "1:25:20" + mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data) + + # Server has been restarted so therefore we should have a new state + freezer.move_to(MOCK_REFERENCE_DATE + timedelta(days=2)) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00" diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 7e57312c5b6..a8a8f67bcc1 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -4,10 +4,10 @@ from unittest.mock import patch from goalzero import exceptions -from homeassistant import data_entry_flow from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN, MANUFACTURER from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -35,7 +35,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -48,7 +48,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -59,7 +59,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -71,7 +71,7 @@ async def test_flow_user_invalid_host(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_host" @@ -83,7 +83,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -98,14 +98,14 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MANUFACTURER assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -115,7 +115,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -129,7 +129,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -139,7 +139,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -149,5 +149,5 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py index 1390561785e..1d44c7e808e 100644 --- a/tests/components/goalzero/test_init.py +++ b/tests/components/goalzero/test_init.py @@ -25,7 +25,7 @@ async def test_setup_config_and_unload(hass: HomeAssistant) -> None: with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -46,7 +46,7 @@ async def test_setup_config_entry_incorrectly_formatted_mac( with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -64,7 +64,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: side_effect=exceptions.ConnectError, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_update_failed( diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index a88dbd45116..25fb5922506 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -57,7 +57,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == { "base": "invalid_auth", } @@ -77,7 +77,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -95,7 +95,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -115,7 +115,7 @@ async def test_form_homekit_unique_id_already_setup(hass: HomeAssistant) -> None type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -143,7 +143,7 @@ async def test_form_homekit_unique_id_already_setup(hass: HomeAssistant) -> None type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_homekit_ip_address_already_setup(hass: HomeAssistant) -> None: @@ -168,7 +168,7 @@ async def test_form_homekit_ip_address_already_setup(hass: HomeAssistant) -> Non type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_homekit_ip_address(hass: HomeAssistant) -> None: @@ -187,7 +187,7 @@ async def test_form_homekit_ip_address(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} data_schema = result["data_schema"] @@ -217,7 +217,7 @@ async def test_discovered_dhcp( ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -229,7 +229,7 @@ async def test_discovered_dhcp( }, ) assert result2 - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -245,7 +245,7 @@ async def test_discovered_dhcp( }, ) assert result3 - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "device": "ismartgate", "ip_address": "1.2.3.4", @@ -270,7 +270,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_init( @@ -280,7 +280,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" result3 = await hass.config_entries.flow.async_init( @@ -290,5 +290,5 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ip="1.2.3.4", macaddress="00:00:00:00:00:00", hostname="mock_hostname" ), ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py index 0d0a1249ea1..bede53ec9ed 100644 --- a/tests/components/goodwe/test_config_flow.py +++ b/tests/components/goodwe/test_config_flow.py @@ -32,7 +32,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -50,7 +50,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -68,7 +68,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -84,7 +84,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -93,7 +93,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -106,5 +106,5 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_error"} diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 989e6690630..bd64a1d8a49 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -241,7 +241,7 @@ def mock_events_list( def _put_result( response: dict[str, Any], - calendar_id: str = None, + calendar_id: str | None = None, exc: ClientError | None = None, ) -> None: if calendar_id is None: @@ -255,7 +255,6 @@ def mock_events_list( json=resp, exc=exc, ) - return return _put_result @@ -268,7 +267,6 @@ def mock_events_list_items( def _put_items(items: list[dict[str, Any]]) -> None: mock_events_list({"items": items}) - return return _put_items @@ -289,7 +287,6 @@ def mock_calendars_list( json=resp, exc=exc, ) - return return _result @@ -312,7 +309,6 @@ def mock_calendar_get( exc=exc, status=status, ) - return return _result @@ -330,7 +326,6 @@ def mock_insert_event( f"{API_BASE_URL}/calendars/{calendar_id}/events", exc=exc, ) - return return _expect_result diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 3946e432497..cf138567ba9 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -79,7 +79,9 @@ class Client: self.client = client self.id = 0 - async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]: + async def cmd( + self, cmd: str, payload: dict[str, Any] | None = None + ) -> dict[str, Any]: """Send a command and receive the json result.""" self.id += 1 await self.client.send_json( @@ -93,7 +95,7 @@ class Client: assert resp.get("id") == self.id return resp - async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any: + async def cmd_result(self, cmd: str, payload: dict[str, Any] | None = None) -> Any: """Send a command and parse the result.""" resp = await self.cmd(cmd, payload) assert resp.get("success") diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index f8eff022d9f..12af97c8604 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -151,7 +151,7 @@ async def test_full_flow_application_creds( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -167,7 +167,7 @@ async def test_full_flow_application_creds( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] @@ -213,7 +213,7 @@ async def test_code_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_error" @@ -233,7 +233,7 @@ async def test_timeout_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "timeout_connect" @@ -252,7 +252,7 @@ async def test_expired_after_exchange( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -267,7 +267,7 @@ async def test_expired_after_exchange( await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "code_expired" @@ -287,7 +287,7 @@ async def test_exchange_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -309,7 +309,7 @@ async def test_exchange_error( # Status has not updated, will retry result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" # Run another tick, which attempts credential exchange again @@ -323,7 +323,7 @@ async def test_exchange_error( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] @@ -373,7 +373,7 @@ async def test_duplicate_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -383,7 +383,7 @@ async def test_duplicate_config_entries( await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -415,7 +415,7 @@ async def test_multiple_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -430,7 +430,7 @@ async def test_multiple_config_entries( result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "another-email@example.com" assert len(mock_setup.mock_calls) == 1 @@ -445,7 +445,7 @@ async def test_missing_configuration( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -471,7 +471,7 @@ async def test_wrong_configuration( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_error" @@ -506,14 +506,14 @@ async def test_reauth_flow( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={}, ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -529,7 +529,7 @@ async def test_reauth_flow( flow_id=result["flow_id"] ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" entries = hass.config_entries.async_entries(DOMAIN) @@ -576,7 +576,7 @@ async def test_calendar_lookup_failure( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -590,7 +590,7 @@ async def test_calendar_lookup_failure( flow_id=result["flow_id"] ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == reason @@ -611,7 +611,7 @@ async def test_options_flow_triggers_reauth( assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"calendar_access"} @@ -622,7 +622,7 @@ async def test_options_flow_triggers_reauth( "calendar_access": "read_only", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"calendar_access": "read_only"} @@ -643,7 +643,7 @@ async def test_options_flow_no_changes( assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -652,7 +652,7 @@ async def test_options_flow_no_changes( "calendar_access": "read_write", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"calendar_access": "read_write"} @@ -687,7 +687,7 @@ async def test_web_auth_compatibility( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == "external" + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -716,7 +716,7 @@ async def test_web_auth_compatibility( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY token = result.get("data", {}).get("token", {}) del token["expires_at"] assert token == { @@ -770,7 +770,7 @@ async def test_web_reauth_flow( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -791,7 +791,7 @@ async def test_web_reauth_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result.get("type") == "external" + assert result.get("type") is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -820,7 +820,7 @@ async def test_web_reauth_flow( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" entries = hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 319f6be5012..2a26776b031 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -116,7 +116,7 @@ async def test_unload_entry( assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -959,4 +959,4 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_remove(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 4198e648b53..648feb1cc8e 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -155,6 +155,7 @@ async def test_sync_request( for dev, demo in zip( sorted(devices, key=lambda d: d["id"]), sorted(DEMO_DEVICES, key=lambda d: d["id"]), + strict=False, ): assert dev["name"] == demo["name"] assert set(dev["traits"]) == set(demo["traits"]) diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 3f7fd91fed2..492f1be1829 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -344,7 +344,10 @@ def test_supported_features_string(caplog: pytest.LogCaptureFixture) -> None: State("test.entity_id", "on", {"supported_features": "invalid"}), ) assert entity.is_supported() is False - assert "Entity test.entity_id contains invalid supported_features value invalid" + assert ( + "Entity test.entity_id contains invalid supported_features value invalid" + in caplog.text + ) def test_request_data() -> None: diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 758ebf63db9..9c8a0f951cc 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -216,6 +216,10 @@ async def test_report_notifications( hass, datetime.fromisoformat("2023-08-01T01:01:00+00:00") ) await hass.async_block_till_done() + for call in mock_report_state.mock_calls: + if "states" in call[1][0]["devices"]: + states = call[1][0]["devices"]["states"] + assert states["event.doorbell"] == {"online": True} # Test the notification request failed caplog.clear() @@ -233,12 +237,10 @@ async def test_report_notifications( hass, datetime.fromisoformat("2023-08-01T01:03:00+00:00") ) await hass.async_block_till_done() - assert len(mock_report_state.mock_calls) == 2 + assert len(mock_report_state.mock_calls) == 1 for call in mock_report_state.mock_calls: if "notifications" in call[1][0]["devices"]: notifications = call[1][0]["devices"]["notifications"] - elif "states" in call[1][0]["devices"]: - states = call[1][0]["devices"]["states"] assert notifications["event.doorbell"] == { "ObjectDetection": { "objects": {"unclassified": 1}, @@ -246,7 +248,6 @@ async def test_report_notifications( "detectionTimestamp": epoc_event_time * 1000, } } - assert states["event.doorbell"] == {"online": True} assert "Sending event notification for entity event.doorbell" in caplog.text assert ( "Unable to send notification with result code: 500, check log for more info" diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index 49e849398af..4a4931d7bae 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.google_assistant_sdk.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .conftest import CLIENT_ID, ComponentSetup @@ -68,7 +69,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == TITLE assert "result" in result assert result.get("result").unique_id is None @@ -144,7 +145,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert config_entry.unique_id is None @@ -206,7 +207,7 @@ async def test_single_instance_allowed( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -221,7 +222,7 @@ async def test_options_flow( # Trigger options flow, first time result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"language_code"} @@ -230,12 +231,12 @@ async def test_options_flow( result["flow_id"], user_input={"language_code": "es-ES"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, not change language result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"language_code"} @@ -244,12 +245,12 @@ async def test_options_flow( result["flow_id"], user_input={"language_code": "es-ES"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, change language result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"language_code"} @@ -258,5 +259,5 @@ async def test_options_flow( result["flow_id"], user_input={"language_code": "en-US"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"language_code": "en-US"} diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 2d930599c24..11b3fbaa03f 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -327,6 +327,7 @@ async def test_conversation_agent( """Test GoogleAssistantConversationAgent.""" await setup_integration() + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) entries = hass.config_entries.async_entries(DOMAIN) @@ -334,7 +335,7 @@ async def test_conversation_agent( entry = entries[0] assert entry.state is ConfigEntryState.LOADED - agent = await conversation._get_agent_manager(hass).async_get_agent(entry.entry_id) + agent = conversation.get_agent_manager(hass).async_get_agent(entry.entry_id) assert agent.supported_languages == SUPPORTED_LANGUAGE_CODES text1 = "tell me a joke" @@ -365,6 +366,7 @@ async def test_conversation_agent_refresh_token( """Test GoogleAssistantConversationAgent when token is expired.""" await setup_integration() + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) entries = hass.config_entries.async_entries(DOMAIN) @@ -416,6 +418,7 @@ async def test_conversation_agent_language_changed( """Test GoogleAssistantConversationAgent when language is changed.""" await setup_integration() + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) entries = hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index 66dfd980cf3..c377a469df0 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -4,6 +4,8 @@ from unittest.mock import patch import pytest +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -23,10 +25,16 @@ def mock_config_entry(hass): @pytest.fixture -async def mock_init_component(hass, mock_config_entry): +async def mock_init_component(hass: HomeAssistant, mock_config_entry: ConfigEntry): """Initialize integration.""" with patch("google.generativeai.get_model"): assert await async_setup_component( hass, "google_generative_ai_conversation", {} ) await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) 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 6ae42a350e6..3bac01db42d 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "api_key": "bla", } @@ -78,7 +78,7 @@ async def test_options( }, ) await hass.async_block_till_done() - assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"]["prompt"] == "Speak like a pirate" assert options["data"]["temperature"] == 0.3 assert options["data"][CONF_CHAT_MODEL] == DEFAULT_CHAT_MODEL @@ -121,5 +121,5 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index b77fa14b4cf..07254be9e3f 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -152,7 +152,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test GoogleGenerativeAIAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index 62db6603988..06479504f9d 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.google_mail.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .conftest import CLIENT_ID, GOOGLE_AUTH_URI, GOOGLE_TOKEN_URI, SCOPES, TITLE @@ -63,7 +64,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == TITLE assert "result" in result assert result.get("result").unique_id == TITLE @@ -160,7 +161,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result["reason"] == abort_reason assert result["description_placeholders"] == placeholders assert len(mock_setup.mock_calls) == calls @@ -212,5 +213,5 @@ async def test_already_configured( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index e397ab2c403..a793ade5312 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -3,11 +3,11 @@ from dataclasses import dataclass from datetime import datetime import os -import unittest.mock as mock +from unittest import mock import pytest -import homeassistant.components.google_pubsub as google_pubsub +from homeassistant.components import google_pubsub from homeassistant.components.google_pubsub import DateTimeJSONEncoder as victim from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index edf4580485f..5d8a19d1b61 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.application_credentials import ( ) from homeassistant.components.google_sheets.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -104,7 +105,7 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 assert len(mock_client.mock_calls) == 2 - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == TITLE assert "result" in result assert result.get("result").unique_id == SHEET_ID @@ -163,7 +164,7 @@ async def test_create_sheet_error( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "create_spreadsheet_failure" @@ -238,7 +239,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert config_entry.unique_id == SHEET_ID @@ -313,7 +314,7 @@ async def test_reauth_abort( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "open_spreadsheet_failure" @@ -376,5 +377,5 @@ async def test_already_configured( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/google_tasks/conftest.py b/tests/components/google_tasks/conftest.py index 87ddb2ed81d..7db78af6232 100644 --- a/tests/components/google_tasks/conftest.py +++ b/tests/components/google_tasks/conftest.py @@ -54,6 +54,7 @@ def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry: """Fixture for a config entry.""" return MockConfigEntry( domain=DOMAIN, + unique_id="123", data={ "auth_implementation": DOMAIN, "token": token_entry, diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index d7ad21292fc..5b2d4f11fee 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -1,9 +1,11 @@ """Test the Google Tasks config flow.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import Mock, patch from googleapiclient.errors import HttpError from httplib2 import Response +import pytest from homeassistant import config_entries from homeassistant.components.google_tasks.const import ( @@ -15,18 +17,37 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker CLIENT_ID = "1234" CLIENT_SECRET = "5678" +@pytest.fixture +def user_identifier() -> str: + """Return a unique user ID.""" + return "123" + + +@pytest.fixture +def setup_userinfo(user_identifier: str) -> Generator[Mock, None, None]: + """Set up userinfo.""" + with patch("homeassistant.components.google_tasks.config_flow.build") as mock: + mock.return_value.userinfo.return_value.get.return_value.execute.return_value = { + "id": user_identifier, + "name": "Test Name", + } + yield mock + + async def test_full_flow( hass: HomeAssistant, hass_client_no_auth, - aioclient_mock, + aioclient_mock: AiohttpClientMocker, current_request_with_host, setup_credentials, + setup_userinfo, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -44,7 +65,8 @@ async def test_full_flow( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/tasks" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" ) @@ -63,14 +85,13 @@ 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: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "123" + assert result["result"].title == "Test Name" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -78,9 +99,10 @@ async def test_full_flow( async def test_api_not_enabled( hass: HomeAssistant, hass_client_no_auth, - aioclient_mock, + aioclient_mock: AiohttpClientMocker, current_request_with_host, setup_credentials, + setup_userinfo, ) -> None: """Check flow aborts if api is not enabled.""" result = await hass.config_entries.flow.async_init( @@ -98,7 +120,8 @@ async def test_api_not_enabled( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/tasks" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" ) @@ -126,7 +149,7 @@ async def test_api_not_enabled( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "access_not_configured" assert ( result["description_placeholders"]["message"] @@ -137,9 +160,10 @@ async def test_api_not_enabled( async def test_general_exception( hass: HomeAssistant, hass_client_no_auth, - aioclient_mock, + aioclient_mock: AiohttpClientMocker, current_request_with_host, setup_credentials, + setup_userinfo, ) -> None: """Check flow aborts if exception happens.""" result = await hass.config_entries.flow.async_init( @@ -157,7 +181,8 @@ async def test_general_exception( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/tasks" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" ) @@ -182,5 +207,110 @@ async def test_general_exception( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" + + +@pytest.mark.parametrize( + ("user_identifier", "abort_reason", "resulting_access_token", "starting_unique_id"), + [ + ( + "123", + "reauth_successful", + "updated-access-token", + "123", + ), + ( + "123", + "reauth_successful", + "updated-access-token", + None, + ), + ( + "345", + "wrong_account", + "mock-access", + "123", + ), + ], +) +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock: AiohttpClientMocker, + current_request_with_host, + setup_credentials, + setup_userinfo, + user_identifier: str, + abort_reason: str, + resulting_access_token: str, + starting_unique_id: str | None, +) -> None: + """Test the re-authentication case updates the correct config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=starting_unique_id, + data={ + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access", + } + }, + ) + 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() + 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}" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" + "&access_type=offline&prompt=consent" + ) + 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.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "updated-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_tasks.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["type"] == "abort" + assert result["reason"] == abort_reason + + assert config_entry.unique_id == "123" + assert "token" in config_entry.data + # Verify access token is refreshed + assert config_entry.data["token"]["access_token"] == resulting_access_token + assert config_entry.data["token"]["refresh_token"] == "mock-refresh-token" diff --git a/tests/components/google_tasks/test_init.py b/tests/components/google_tasks/test_init.py index 061bf549748..1fe0e4a0c36 100644 --- a/tests/components/google_tasks/test_init.py +++ b/tests/components/google_tasks/test_init.py @@ -30,7 +30,7 @@ async def test_setup( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.services.async_services().get(DOMAIN) @@ -68,7 +68,7 @@ async def test_expired_token_refresh_success( ( time.time() - 3600, http.HTTPStatus.UNAUTHORIZED, - ConfigEntryState.SETUP_RETRY, # Will trigger reauth in the future + ConfigEntryState.SETUP_ERROR, ), ( time.time() - 3600, diff --git a/tests/components/google_translate/test_config_flow.py b/tests/components/google_translate/test_config_flow.py index a4104fc0908..36399c6770a 100644 --- a/tests/components/google_translate/test_config_flow.py +++ b/tests/components/google_translate/test_config_flow.py @@ -20,7 +20,7 @@ async def test_user_step(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -32,7 +32,7 @@ async def test_user_step(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Google Translate text-to-speech" assert result["data"] == { CONF_LANG: "de", @@ -53,7 +53,7 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -64,7 +64,7 @@ async def test_already_configured( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -77,7 +77,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Google Translate text-to-speech" assert result.get("data") == { CONF_LANG: "en", diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 1df609b0db4..1cff6e97781 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -73,6 +73,8 @@ async def setup_fixture( else: raise RuntimeError("Invalid setup fixture") + await hass.async_block_till_done() + @pytest.fixture(name="config") def config_fixture() -> dict[str, Any]: diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 24e9cb1297a..6e73bfd8d23 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -2,7 +2,7 @@ import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.google_travel_time.const import ( ARRIVAL_TIME, CONF_ARRIVAL_TIME, @@ -23,6 +23,7 @@ from homeassistant.components.google_travel_time.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import MOCK_CONFIG @@ -33,7 +34,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -41,7 +42,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -57,14 +58,14 @@ async def test_invalid_config_entry(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,14 +75,14 @@ async def test_invalid_api_key(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -91,14 +92,14 @@ async def test_transport_error(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -108,14 +109,14 @@ async def test_timeout(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout_connect"} @@ -124,14 +125,14 @@ async def test_malformed_api_key(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -154,7 +155,7 @@ async def test_options_flow(hass: HomeAssistant, mock_config) -> None: mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -171,7 +172,7 @@ async def test_options_flow(hass: HomeAssistant, mock_config) -> None: CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -215,7 +216,7 @@ async def test_options_flow_departure_time(hass: HomeAssistant, mock_config) -> mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -232,7 +233,7 @@ async def test_options_flow_departure_time(hass: HomeAssistant, mock_config) -> CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -285,7 +286,7 @@ async def test_reset_departure_time(hass: HomeAssistant, mock_config) -> None: mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -331,7 +332,7 @@ async def test_reset_arrival_time(hass: HomeAssistant, mock_config) -> None: mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -375,7 +376,7 @@ async def test_reset_options_flow_fields(hass: HomeAssistant, mock_config) -> No mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -401,7 +402,7 @@ async def test_dupe(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -413,13 +414,13 @@ async def test_dupe(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -432,4 +433,4 @@ async def test_dupe(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index 4b498b2618a..0c340c01f2a 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5075_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.govee_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5075 2762" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_govee(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_GOVEE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.govee_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.govee_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/govee_light_local/test_config_flow.py b/tests/components/govee_light_local/test_config_flow.py index 79baef33969..1f935f18530 100644 --- a/tests/components/govee_light_local/test_config_flow.py +++ b/tests/components/govee_light_local/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import AsyncMock, patch from govee_local_api import GoveeDevice -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.govee_light_local.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import DEFAULT_CAPABILITEIS @@ -33,10 +34,10 @@ async def test_creating_entry_has_no_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() @@ -70,10 +71,10 @@ async def test_creating_entry_has_with_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/gpsd/test_config_flow.py b/tests/components/gpsd/test_config_flow.py index 81d6681dabd..6f330571076 100644 --- a/tests/components/gpsd/test_config_flow.py +++ b/tests/components/gpsd/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from gps3.agps3threaded import GPSD_PORT as DEFAULT_PORT -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.gpsd.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("socket.socket") as mock_socket: mock_connect = mock_socket.return_value.connect @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"GPS {HOST}" assert result2["data"] == { CONF_HOST: HOST, @@ -53,7 +53,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: data={CONF_HOST: "nonexistent.local", CONF_PORT: 1234}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -68,7 +68,7 @@ async def test_import(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: 1234, CONF_NAME: "MyGPS"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "MyGPS" assert result["data"] == { CONF_HOST: HOST, diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index cfe9d050c69..988581c804a 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -5,13 +5,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import gpslogger, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_HOME, STATE_NOT_HOME 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 homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -65,10 +66,10 @@ async def webhook_id(hass, gpslogger_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index 97656596ce6..ca217168b18 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -69,7 +69,7 @@ def build_device_info_mock( def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233"): """Build mock device object.""" - mock = Mock( + return Mock( device_info=build_device_info_mock(name, ipAddress, mac), name=name, bind=AsyncMock(), @@ -89,7 +89,6 @@ def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc1122 power_save=False, steady_heat=False, ) - return mock async def async_setup_gree(hass: HomeAssistant) -> MockConfigEntry: diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index 7127af6b913..af374fb4245 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import FakeDiscovery @@ -27,10 +28,10 @@ async def test_creating_entry_sets_up_climate( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -53,10 +54,10 @@ async def test_creating_entry_has_no_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index d09d31d1db8..8d25a671806 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -1,5 +1,6 @@ """Common fixtures for testing greeneye_monitor.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -98,17 +99,18 @@ def assert_sensor_registered( @pytest.fixture -def monitors() -> AsyncMock: +def monitors() -> Generator[AsyncMock, None, None]: """Provide a mock greeneye.Monitors object that has listeners and can add new monitors.""" - with patch("greeneye.Monitors", new=AsyncMock) as mock_monitors: - add_listeners(mock_monitors) - mock_monitors.monitors = {} + with patch("greeneye.Monitors", autospec=True) as mock_monitors: + mock = mock_monitors.return_value + add_listeners(mock) + mock.monitors = {} def add_monitor(monitor: MagicMock) -> None: """Add the given mock monitor as a monitor with the given serial number, notifying any listeners on the Monitors object.""" serial_number = monitor.serial_number - mock_monitors.monitors[serial_number] = monitor - mock_monitors.notify_all_listeners(monitor) + mock.monitors[serial_number] = monitor + mock.notify_all_listeners(monitor) - mock_monitors.add_monitor = add_monitor - yield mock_monitors + mock.add_monitor = add_monitor + yield mock diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 623bcb5c1ee..3aea9d21f0c 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -76,14 +76,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type with patch( @@ -99,7 +99,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room" assert result["data"] == {} assert result["options"] == { @@ -170,14 +170,14 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.flow.async_configure( @@ -191,7 +191,7 @@ async def test_config_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entity_registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert entity_registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -261,7 +261,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") == members1 assert "name" not in result["data_schema"].schema @@ -273,7 +273,7 @@ async def test_options( result["flow_id"], user_input={"entities": members2, **options_options}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -300,14 +300,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") is None @@ -357,7 +357,7 @@ async def test_all_options( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": advanced} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.options.async_configure( @@ -366,7 +366,7 @@ async def test_all_options( "entities": members2, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -454,7 +454,7 @@ async def test_options_flow_hides_members( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(group_config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -465,7 +465,7 @@ async def test_options_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entity_registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert entity_registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -518,14 +518,14 @@ async def test_config_flow_preview( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": domain}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == domain assert result["errors"] is None assert result["preview"] == "group" @@ -540,6 +540,7 @@ async def test_config_flow_preview( } ) msg = await client.receive_json() + preview_subscribe_id = msg["id"] assert msg["success"] assert msg["result"] is None @@ -549,9 +550,32 @@ async def test_config_flow_preview( "state": "unavailable", } + await client.send_json_auto_id( + { + "type": "unsubscribe_events", + "subscription": preview_subscribe_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + hass.states.async_set(input_entities[0], input_states[0]) hass.states.async_set(input_entities[1], input_states[1]) + await client.send_json_auto_id( + { + "type": "group/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My group", "entities": input_entities} + | extra_user_input, + } + ) + msg = await client.receive_json() + preview_subscribe_id = msg["id"] + assert msg["success"] + assert msg["result"] is None + msg = await client.receive_json() assert msg["event"] == { "attributes": { @@ -626,7 +650,7 @@ async def test_option_flow_preview( client = await hass_ws_client(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "group" @@ -681,7 +705,7 @@ async def test_option_flow_sensor_preview_config_entry_removed( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "group" diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 45846123a80..d3f2747933e 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -2,24 +2,29 @@ from __future__ import annotations +import asyncio from collections import OrderedDict from typing import Any from unittest.mock import patch import pytest -import homeassistant.components.group as group +from homeassistant.components import group from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_ICON, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, + STATE_CLOSED, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, + STATE_OPEN, STATE_UNKNOWN, + STATE_UNLOCKED, ) from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import entity_registry as er @@ -603,6 +608,108 @@ async def test_is_on(hass: HomeAssistant) -> None: assert not group.is_on(hass, "non.existing") +@pytest.mark.parametrize( + ( + "domains", + "states_old", + "states_new", + "state_ison_group_old", + "state_ison_group_new", + ), + [ + ( + ("light", "light"), + (STATE_ON, STATE_OFF), + (STATE_OFF, STATE_OFF), + (STATE_ON, True), + (STATE_OFF, False), + ), + ( + ("cover", "cover"), + (STATE_OPEN, STATE_CLOSED), + (STATE_CLOSED, STATE_CLOSED), + (STATE_OPEN, True), + (STATE_CLOSED, False), + ), + ( + ("lock", "lock"), + (STATE_UNLOCKED, STATE_LOCKED), + (STATE_LOCKED, STATE_LOCKED), + (STATE_UNLOCKED, True), + (STATE_LOCKED, False), + ), + ( + ("cover", "lock"), + (STATE_OPEN, STATE_LOCKED), + (STATE_CLOSED, STATE_LOCKED), + (STATE_ON, True), + (STATE_OFF, False), + ), + ( + ("cover", "lock"), + (STATE_OPEN, STATE_UNLOCKED), + (STATE_CLOSED, STATE_LOCKED), + (STATE_ON, True), + (STATE_OFF, False), + ), + ( + ("cover", "lock", "light"), + (STATE_OPEN, STATE_LOCKED, STATE_ON), + (STATE_CLOSED, STATE_LOCKED, STATE_OFF), + (STATE_ON, True), + (STATE_OFF, False), + ), + ], +) +async def test_is_on_and_state_mixed_domains( + hass: HomeAssistant, + domains: tuple[str, ...], + states_old: tuple[str, ...], + states_new: tuple[str, ...], + state_ison_group_old: tuple[str, bool], + state_ison_group_new: tuple[str, bool], +) -> None: + """Test is_on method with mixed domains.""" + count = len(domains) + entity_ids = [f"{domains[index]}.test_{index}" for index in range(count)] + for index in range(count): + hass.states.async_set(entity_ids[index], states_old[index]) + + assert not group.is_on(hass, "group.none") + await asyncio.gather( + *[async_setup_component(hass, domain, {}) for domain in set(domains)] + ) + assert await async_setup_component(hass, "group", {}) + await hass.async_block_till_done() + + test_group = await group.Group.async_create_group( + hass, + "init_group", + created_by_service=True, + entity_ids=entity_ids, + icon=None, + mode=None, + object_id=None, + order=None, + ) + await hass.async_block_till_done() + + # Assert on old state + state = hass.states.get(test_group.entity_id) + assert state is not None + assert state.state == state_ison_group_old[0] + assert group.is_on(hass, test_group.entity_id) == state_ison_group_old[1] + + # Switch and assert on new state + for index in range(count): + hass.states.async_set(entity_ids[index], states_new[index]) + await hass.async_block_till_done() + state = hass.states.get(test_group.entity_id) + assert state is not None + assert state.state == state_ison_group_new[0] + assert group.is_on(hass, test_group.entity_id) == state_ison_group_new[1] + + async def test_reloading_groups(hass: HomeAssistant) -> None: """Test reloading the group config.""" assert await async_setup_component( diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 52d049431d8..dfd200a1542 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,31 +1,87 @@ """The tests for the notify.group platform.""" -from unittest.mock import MagicMock, patch +from collections.abc import Mapping +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, call, patch from homeassistant import config as hass_config -import homeassistant.components.demo.notify as demo +from homeassistant.components import notify from homeassistant.components.group import SERVICE_RELOAD -import homeassistant.components.group.notify as group -import homeassistant.components.notify as notify from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from tests.common import MockPlatform, get_fixture_path, mock_platform -async def test_send_message_with_data(hass: HomeAssistant) -> None: +class MockNotifyPlatform(MockPlatform): + """Help to set up a legacy test notify platform.""" + + def __init__(self, async_get_service: Any) -> None: + """Initialize platform.""" + super().__init__() + self.async_get_service = async_get_service + + +def mock_notify_platform( + hass: HomeAssistant, + tmp_path: Path, + async_get_service: Any = None, +): + """Specialize the mock platform for legacy notify service.""" + loaded_platform = MockNotifyPlatform(async_get_service) + mock_platform(hass, "test.notify", loaded_platform) + + return loaded_platform + + +async def help_setup_notify( + hass: HomeAssistant, + tmp_path: Path, + targets: dict[str, None] | None = None, + group_setup: list[dict[str, None]] | None = None, +) -> MagicMock: + """Help set up a platform notify service.""" + send_message_mock = MagicMock() + + class _TestNotifyService(notify.BaseNotificationService): + def __init__(self, targets: dict[str, None] | None) -> None: + """Initialize service.""" + self._targets = targets + super().__init__() + + @property + def targets(self) -> Mapping[str, Any] | None: + """Return a dictionary of registered targets.""" + return self._targets + + def send_message(self, message: str, **kwargs: Any) -> None: + """Send a message.""" + send_message_mock(message, kwargs) + + async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> notify.BaseNotificationService: + """Get notify service for mocked platform.""" + return _TestNotifyService(targets) + + # Mock platform with service + mock_notify_platform(hass, tmp_path, async_get_service=async_get_service) + # Setup the platform + items: list[dict[str, Any]] = [{"platform": "test"}] + items.extend(group_setup or []) + await async_setup_component(hass, "notify", {"notify": items}) + await hass.async_block_till_done() + + # Return mock for assertion service calls + return send_message_mock + + +async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> None: """Test sending a message with to a notify group.""" - service1 = demo.DemoNotificationService(hass) - service2 = demo.DemoNotificationService(hass) - - service1.send_message = MagicMock(autospec=True) - service2.send_message = MagicMock(autospec=True) - - def mock_get_service(hass, config, discovery_info=None): - if config["name"] == "demo1": - return service1 - return service2 - assert await async_setup_component( hass, "group", @@ -33,79 +89,95 @@ async def test_send_message_with_data(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with patch.object(demo, "get_service", mock_get_service): - await async_setup_component( - hass, - notify.DOMAIN, - { - "notify": [ - {"name": "demo1", "platform": "demo"}, - {"name": "demo2", "platform": "demo"}, - ] - }, - ) - await hass.async_block_till_done() - - service = await group.async_get_service( - hass, + group_setup = [ { + "platform": "group", + "name": "My notification group", "services": [ - {"service": "demo1"}, + {"service": "test_service1"}, { - "service": "demo2", + "service": "test_service2", "data": { "target": "unnamed device", "data": {"test": "message", "default": "default"}, }, }, - ] + ], + } + ] + send_message_mock = await help_setup_notify( + hass, tmp_path, {"service1": 1, "service2": 2}, group_setup + ) + assert hass.services.has_service("notify", "my_notification_group") + + # Test sending a message to a notify group. + await hass.services.async_call( + "notify", + "my_notification_group", + {"message": "Hello", "title": "Test notification", "data": {"hello": "world"}}, + blocking=True, + ) + send_message_mock.assert_has_calls( + [ + call( + "Hello", + { + "title": "Test notification", + "target": [1], + "data": {"hello": "world"}, + }, + ), + call( + "Hello", + { + "title": "Test notification", + "target": [2], + "data": {"hello": "world", "test": "message", "default": "default"}, + }, + ), + ] + ) + send_message_mock.reset_mock() + + # Test sending a message which overrides service defaults to a notify group + await hass.services.async_call( + "notify", + "my_notification_group", + { + "message": "Hello", + "title": "Test notification", + "data": {"hello": "world", "default": "override"}, }, + blocking=True, + ) + send_message_mock.assert_has_calls( + [ + call( + "Hello", + { + "title": "Test notification", + "target": [1], + "data": {"hello": "world", "default": "override"}, + }, + ), + call( + "Hello", + { + "title": "Test notification", + "target": [2], + "data": { + "hello": "world", + "test": "message", + "default": "override", + }, + }, + ), + ] ) - """Test sending a message to a notify group.""" - await service.async_send_message( - "Hello", title="Test notification", data={"hello": "world"} - ) - await hass.async_block_till_done() - - assert service1.send_message.mock_calls[0][1][0] == "Hello" - assert service1.send_message.mock_calls[0][2] == { - "title": "Test notification", - "data": {"hello": "world"}, - } - assert service2.send_message.mock_calls[0][1][0] == "Hello" - assert service2.send_message.mock_calls[0][2] == { - "target": ["unnamed device"], - "title": "Test notification", - "data": {"hello": "world", "test": "message", "default": "default"}, - } - - """Test sending a message which overrides service defaults to a notify group.""" - await service.async_send_message( - "Hello", - title="Test notification", - data={"hello": "world", "default": "override"}, - ) - - await hass.async_block_till_done() - - assert service1.send_message.mock_calls[1][1][0] == "Hello" - assert service1.send_message.mock_calls[1][2] == { - "title": "Test notification", - "data": {"hello": "world", "default": "override"}, - } - assert service2.send_message.mock_calls[1][1][0] == "Hello" - assert service2.send_message.mock_calls[1][2] == { - "target": ["unnamed device"], - "title": "Test notification", - "data": {"hello": "world", "test": "message", "default": "override"}, - } - - -async def test_reload_notify(hass: HomeAssistant) -> None: +async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: """Verify we can reload the notify service.""" - assert await async_setup_component( hass, "group", @@ -113,25 +185,21 @@ async def test_reload_notify(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert await async_setup_component( + await help_setup_notify( hass, - notify.DOMAIN, - { - notify.DOMAIN: [ - {"name": "demo1", "platform": "demo"}, - {"name": "demo2", "platform": "demo"}, - { - "name": "group_notify", - "platform": "group", - "services": [{"service": "demo1"}], - }, - ] - }, + tmp_path, + {"service1": 1, "service2": 2}, + [ + { + "name": "group_notify", + "platform": "group", + "services": [{"service": "test_service1"}], + } + ], ) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "demo1") - assert hass.services.has_service(notify.DOMAIN, "demo2") + assert hass.services.has_service(notify.DOMAIN, "test_service1") + assert hass.services.has_service(notify.DOMAIN, "test_service2") assert hass.services.has_service(notify.DOMAIN, "group_notify") yaml_path = get_fixture_path("configuration.yaml", "group") @@ -145,7 +213,7 @@ async def test_reload_notify(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "demo1") - assert hass.services.has_service(notify.DOMAIN, "demo2") + assert hass.services.has_service(notify.DOMAIN, "test_service1") + assert hass.services.has_service(notify.DOMAIN, "test_service2") assert not hass.services.has_service(notify.DOMAIN, "group_notify") assert hass.services.has_service(notify.DOMAIN, "new_group_notify") diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index fb3c1b6d215..4a8c434c742 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -85,7 +85,7 @@ async def test_sensors2( entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set( entity_id, value, @@ -135,7 +135,7 @@ async def test_sensors_attributes_defined(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set( entity_id, value, @@ -269,7 +269,7 @@ async def test_sensor_incorrect_state_with_ignore_non_numeric( entity_ids = config["sensor"]["entities"] # Check that the final sensor value ignores the non numeric input - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -280,7 +280,7 @@ async def test_sensor_incorrect_state_with_ignore_non_numeric( ) # Check that the final sensor value with all numeric inputs - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -310,7 +310,7 @@ async def test_sensor_incorrect_state_with_not_ignore_non_numeric( entity_ids = config["sensor"]["entities"] # Check that the final sensor value is unavailable if a non numeric input exists - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -319,7 +319,7 @@ async def test_sensor_incorrect_state_with_not_ignore_non_numeric( assert "Unable to use state. Only numerical states are supported" in caplog.text # Check that the final sensor value is correct with all numeric inputs - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -346,7 +346,7 @@ async def test_sensor_require_all_states(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -755,7 +755,7 @@ async def test_last_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() state = hass.states.get("sensor.test_last") diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 57777a57783..e17ea90047b 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -3,7 +3,7 @@ from copy import deepcopy from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.growatt_server.const import ( CONF_PLANT_ID, DEFAULT_URL, @@ -12,6 +12,7 @@ from homeassistant.components.growatt_server.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -52,7 +53,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -70,7 +71,7 @@ async def test_incorrect_login(hass: HomeAssistant) -> None: result["flow_id"], FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -92,7 +93,7 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: result["flow_id"], user_input ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_plants" @@ -116,7 +117,7 @@ async def test_multiple_plant_ids(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plant" user_input = {CONF_PLANT_ID: "123456"} @@ -125,7 +126,7 @@ async def test_multiple_plant_ids(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" @@ -153,7 +154,7 @@ async def test_one_plant_on_account(hass: HomeAssistant) -> None: result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" @@ -179,5 +180,5 @@ async def test_existing_plant_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index 3922b196e4b..0f99578768a 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import patch from aioguardian.errors import GuardianError import pytest -from homeassistant import data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.components.guardian.config_flow import ( @@ -16,6 +15,7 @@ from homeassistant.components.guardian.config_flow import ( from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +29,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -42,7 +42,7 @@ async def test_connect_error(hass: HomeAssistant, config) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -63,13 +63,13 @@ async def test_step_user(hass: HomeAssistant, config, setup_guardian) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -93,13 +93,13 @@ async def test_step_zeroconf(hass: HomeAssistant, setup_guardian) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -123,13 +123,13 @@ async def test_step_zeroconf_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -144,13 +144,13 @@ async def test_step_dhcp(hass: HomeAssistant, setup_guardian) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -170,13 +170,13 @@ async def test_step_dhcp_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -196,7 +196,7 @@ async def test_step_dhcp_already_setup_match_mac(hass: HomeAssistant) -> None: macaddress="aabbccddabcd", ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -218,5 +218,5 @@ async def test_step_dhcp_already_setup_match_ip(hass: HomeAssistant) -> None: macaddress="aabbccddabcd", ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index fe5ddcacdea..4dfc696daf2 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -7,6 +7,7 @@ from aiohttp import ClientResponseError from homeassistant import config_entries from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_obj = MagicMock() @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Default username" assert result2["data"] == { "url": DEFAULT_URL, @@ -75,7 +76,7 @@ async def test_form_invalid_credentials(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_credentials"} @@ -101,7 +102,7 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -117,7 +118,7 @@ async def test_manual_flow_config_exist(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_obj = MagicMock() @@ -136,5 +137,5 @@ async def test_manual_flow_config_exist(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py index 1965e2af8c7..998c024dc72 100644 --- a/tests/components/hardkernel/test_config_flow.py +++ b/tests/components/hardkernel/test_config_flow.py @@ -21,7 +21,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hardkernel" assert result["data"] == {} assert result["options"] == {} @@ -54,6 +54,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/hardkernel/test_init.py b/tests/components/hardkernel/test_init.py index 90717054ead..af01006afba 100644 --- a/tests/components/hardkernel/test_init.py +++ b/tests/components/hardkernel/test_init.py @@ -101,4 +101,4 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index c2daa98728b..d87bfd32326 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -4,12 +4,13 @@ from unittest.mock import AsyncMock, MagicMock, patch import aiohttp -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -28,7 +29,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} harmonyapi = _get_mock_harmonyapi(connect=True) @@ -48,7 +49,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "friend" assert result2["data"] == {"host": "1.2.3.4", "name": "friend"} assert len(mock_setup_entry.mock_calls) == 1 @@ -73,7 +74,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} assert result["description_placeholders"] == { @@ -103,7 +104,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Harmony Hub" assert result2["data"] == {"host": "192.168.1.12", "name": "Harmony Hub"} assert len(mock_setup_entry.mock_calls) == 1 @@ -128,7 +129,7 @@ async def test_form_ssdp_fails_to_get_remote_id(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -167,7 +168,7 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known( }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -190,7 +191,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -211,7 +212,7 @@ async def test_options_flow(hass: HomeAssistant, mock_hc, mock_write_config) -> assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -219,7 +220,7 @@ async def test_options_flow(hass: HomeAssistant, mock_hc, mock_write_config) -> user_input={"activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4, diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py index 1c56f4e25f5..1153203817d 100644 --- a/tests/components/hassio/test_config_flow.py +++ b/tests/components/hassio/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant.components.hassio import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_config_flow(hass: HomeAssistant) -> None: @@ -21,7 +22,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Supervisor" assert result["data"] == {} await hass.async_block_till_done() @@ -36,5 +37,5 @@ async def test_multiple_entries(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 27e99f7f596..805b5292edb 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -379,8 +379,8 @@ async def test_ingress_request_get_compressed( # Check we got right response assert resp.status == HTTPStatus.OK - body = await resp.text() - assert body == body + resp_body = await resp.text() + assert resp_body == body assert resp.headers["Content-Encoding"] == "deflate" # Check we forwarded command diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index da49b8d9f16..572593d642b 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1098,7 +1098,7 @@ async def test_setup_hardware_integration( ) as mock_setup_entry, ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert result assert aioclient_mock.call_count == 19 diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 2dffba74fef..33d266eb24b 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -6,7 +6,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.core import HomeAssistant import homeassistant.helpers.issue_registry as ir from homeassistant.setup import async_setup_component @@ -18,12 +17,6 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator -@pytest.fixture(autouse=True) -async def setup_repairs(hass: HomeAssistant): - """Set up the repairs integration.""" - assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) - - @pytest.fixture(autouse=True) async def fixture_supervisor_environ(): """Mock os environ for supervisor.""" diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 82ac3eccdf5..55cec90ec58 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.hassio import ( HassioAPIError, ) from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -317,7 +318,7 @@ async def test_stats_addon_sensor( 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 + assert config_entry.state is ConfigEntryState.LOADED # Verify the entity is still enabled assert entity_registry.async_get(entity_id).disabled_by is None diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 7f0cd6cbd5a..7b737d7bb4b 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -5,13 +5,13 @@ from urllib.parse import urlparse from pyheos import HeosError -from homeassistant import data_entry_flow from homeassistant.components import heos, ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_aborts_already_setup(hass: HomeAssistant, config_entry) -> None: @@ -20,7 +20,7 @@ async def test_flow_aborts_already_setup(hass: HomeAssistant, config_entry) -> N flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -29,7 +29,7 @@ async def test_no_host_shows_form(hass: HomeAssistant) -> None: flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -40,7 +40,7 @@ async def test_cannot_connect_shows_error_form(hass: HomeAssistant, controller) result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "cannot_connect" assert controller.connect.call_count == 1 @@ -56,7 +56,7 @@ async def test_create_entry_when_host_valid(hass: HomeAssistant, controller) -> result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data @@ -74,7 +74,7 @@ async def test_create_entry_when_friendly_name_valid( result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} @@ -122,7 +122,7 @@ async def test_discovery_flow_aborts_already_setup( flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_ssdp(discovery_data) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -155,5 +155,5 @@ async def test_import_sets_the_unique_id(hass: HomeAssistant, controller) -> Non data={CONF_HOST: "127.0.0.2"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 51b12978856..eb958991c71 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from here_routing import HERERoutingError, HERERoutingUnauthorizedError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.here_travel_time.const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, @@ -22,6 +22,7 @@ from homeassistant.components.here_travel_time.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( API_KEY, @@ -46,7 +47,7 @@ def bypass_setup_fixture(): @pytest.fixture(name="user_step_result") -async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: +async def user_step_result_fixture(hass: HomeAssistant) -> FlowResultType: """Provide the result of a completed user step.""" init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -64,7 +65,7 @@ async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowR @pytest.fixture(name="option_init_result") -async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: +async def option_init_result_fixture(hass: HomeAssistant) -> FlowResultType: """Provide the result of a completed options init step.""" entry = MockConfigEntry( domain=DOMAIN, @@ -83,25 +84,24 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() flow = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( + return await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, }, ) - return result @pytest.fixture(name="origin_step_result") async def origin_step_result_fixture( - hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult -) -> data_entry_flow.FlowResult: + hass: HomeAssistant, user_step_result: FlowResultType +) -> FlowResultType: """Provide the result of a completed origin by coordinates step.""" origin_menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - location_selector_result = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( origin_menu_result["flow_id"], { "origin": { @@ -111,7 +111,6 @@ async def origin_step_result_fixture( } }, ) - return location_selector_result @pytest.mark.parametrize( @@ -124,7 +123,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -137,19 +136,19 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["menu_options"] == menu_options @pytest.mark.usefixtures("valid_response") async def test_step_origin_coordinates( - hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult + hass: HomeAssistant, user_step_result: FlowResultType ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -161,35 +160,35 @@ async def test_step_origin_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.FlowResultType.MENU + assert location_selector_result["type"] is FlowResultType.MENU @pytest.mark.usefixtures("valid_response") async def test_step_origin_entity( - hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult + hass: HomeAssistant, user_step_result: FlowResultType ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_entity"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"origin_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.FlowResultType.MENU + assert entity_selector_result["type"] is FlowResultType.MENU @pytest.mark.usefixtures("valid_response") async def test_step_destination_coordinates( - hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult + hass: HomeAssistant, origin_step_result: FlowResultType ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -201,9 +200,7 @@ async def test_step_destination_coordinates( } }, ) - assert ( - location_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - ) + assert location_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -219,19 +216,19 @@ async def test_step_destination_coordinates( @pytest.mark.usefixtures("valid_response") async def test_step_destination_entity( hass: HomeAssistant, - origin_step_result: data_entry_flow.FlowResult, + origin_step_result: FlowResultType, ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_entity"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"destination_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert entity_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -267,7 +264,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -290,7 +287,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -310,7 +307,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -320,18 +317,18 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU @pytest.mark.usefixtures("valid_response") async def test_options_flow_arrival_time_step( - hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult + hass: HomeAssistant, option_init_result: FlowResultType ) -> None: """Test the options flow arrival time type.""" menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "arrival_time"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -339,7 +336,7 @@ async def test_options_flow_arrival_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert time_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, @@ -349,13 +346,13 @@ async def test_options_flow_arrival_time_step( @pytest.mark.usefixtures("valid_response") async def test_options_flow_departure_time_step( - hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult + hass: HomeAssistant, option_init_result: FlowResultType ) -> None: """Test the options flow departure time type.""" menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "departure_time"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -363,7 +360,7 @@ async def test_options_flow_departure_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert time_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, @@ -373,14 +370,14 @@ async def test_options_flow_departure_time_step( @pytest.mark.usefixtures("valid_response") async def test_options_flow_no_time_step( - hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult + hass: HomeAssistant, option_init_result: FlowResultType ) -> None: """Test the options flow arrival time type.""" menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "no_time"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert menu_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index 1abac3421a6..c80e0d28709 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -4,9 +4,10 @@ from unittest.mock import patch from pyaehw4a1 import exceptions -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import hisense_aehw4a1 from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -27,10 +28,10 @@ async def test_creating_entry_sets_up_climate_discovery(hass: HomeAssistant) -> ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 13574bb2bb2..d0712b968bc 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -87,7 +87,7 @@ def test_get_significant_states_minimal_response(hass_history) -> None: 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( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") @@ -162,13 +162,11 @@ def test_get_significant_states_without_initial(hass_history) -> None: 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], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] hist = get_significant_states( diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index 0bbd913ce2b..2e26256da90 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -96,7 +96,7 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None: 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( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") @@ -148,7 +148,7 @@ def test_get_significant_states_with_initial(legacy_hass_history) -> None: if entity_id == "media_player.test": states[entity_id] = states[entity_id][1:] for state in states[entity_id]: - if state.last_changed == one or state.last_changed == one_with_microsecond: + if state.last_changed in (one, one_with_microsecond): state.last_changed = one_and_half state.last_updated = one_and_half @@ -177,8 +177,7 @@ def test_get_significant_states_without_initial(legacy_hass_history) -> None: for entity_id in states: states[entity_id] = list( filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, + lambda s: s.last_changed not in (one, one_with_microsecond), states[entity_id], ) ) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 1982ec12188..4b4592c2104 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1175,53 +1175,6 @@ async def test_measure_sliding_window( ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor1", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor2", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "time", - "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor3", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor4", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with ( patch( "homeassistant.components.recorder.history.state_changes_during_period", @@ -1229,6 +1182,52 @@ async def test_measure_sliding_window( ), freeze_time(start_time), ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() @@ -1377,9 +1376,12 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), ): await async_setup_component( hass, diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index b1553b2c485..fd6eb564a39 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from apyhiveapi.helper import hive_exceptions -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.hive.const import CONF_CODE, CONF_DEVICE_NAME, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -52,7 +53,7 @@ async def test_import_flow(hass: HomeAssistant) -> None: data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"] == { CONF_USERNAME: USERNAME, @@ -76,7 +77,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -104,7 +105,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME assert result2["data"] == { CONF_USERNAME: USERNAME, @@ -129,7 +130,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -146,7 +147,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -167,7 +168,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -200,7 +201,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == USERNAME assert result4["data"] == { CONF_USERNAME: USERNAME, @@ -254,7 +255,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -278,7 +279,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -313,7 +314,7 @@ async def test_reauth_2fa_flow(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -356,7 +357,7 @@ async def test_reauth_2fa_flow(hass: HomeAssistant) -> None: assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -388,14 +389,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL @@ -405,7 +406,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -422,7 +423,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -437,7 +438,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {} @@ -458,7 +459,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: }, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {} @@ -488,7 +489,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == USERNAME assert result5["data"] == { CONF_USERNAME: USERNAME, @@ -530,7 +531,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -540,7 +541,7 @@ async def test_user_flow_invalid_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -552,7 +553,7 @@ async def test_user_flow_invalid_username(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_username"} @@ -563,7 +564,7 @@ async def test_user_flow_invalid_password(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -575,7 +576,7 @@ async def test_user_flow_invalid_password(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_password"} @@ -587,7 +588,7 @@ async def test_user_flow_no_internet_connection(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -599,7 +600,7 @@ async def test_user_flow_no_internet_connection(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "no_internet_available"} @@ -611,7 +612,7 @@ async def test_user_flow_2fa_no_internet_connection(hass: HomeAssistant) -> None DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -625,7 +626,7 @@ async def test_user_flow_2fa_no_internet_connection(hass: HomeAssistant) -> None {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -638,7 +639,7 @@ async def test_user_flow_2fa_no_internet_connection(hass: HomeAssistant) -> None {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "no_internet_available"} @@ -649,7 +650,7 @@ async def test_user_flow_2fa_invalid_code(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -663,7 +664,7 @@ async def test_user_flow_2fa_invalid_code(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -675,7 +676,7 @@ async def test_user_flow_2fa_invalid_code(hass: HomeAssistant) -> None: result["flow_id"], {CONF_CODE: MFA_INVALID_CODE}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "invalid_code"} @@ -686,7 +687,7 @@ async def test_user_flow_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -699,7 +700,7 @@ async def test_user_flow_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -709,7 +710,7 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -723,7 +724,7 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE with patch( @@ -735,7 +736,7 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -759,6 +760,6 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {"base": "unknown"} diff --git a/tests/components/hko/test_config_flow.py b/tests/components/hko/test_config_flow.py index ce32d2cd0da..7a2cec961db 100644 --- a/tests/components/hko/test_config_flow.py +++ b/tests/components/hko/test_config_flow.py @@ -17,7 +17,7 @@ async def test_config_flow_default(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert "flow_id" in result @@ -26,7 +26,7 @@ async def test_config_flow_default(hass: HomeAssistant) -> None: user_input={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_LOCATION assert result2["result"].unique_id == DEFAULT_LOCATION assert result2["data"][CONF_LOCATION] == DEFAULT_LOCATION @@ -42,7 +42,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" client_mock.side_effect = None @@ -53,7 +53,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DEFAULT_LOCATION assert result["data"][CONF_LOCATION] == DEFAULT_LOCATION @@ -68,7 +68,7 @@ async def test_config_flow_timeout(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "unknown" client_mock.side_effect = None @@ -79,7 +79,7 @@ async def test_config_flow_timeout(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DEFAULT_LOCATION assert result["data"][CONF_LOCATION] == DEFAULT_LOCATION @@ -89,24 +89,24 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: r1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert r1["type"] == FlowResultType.FORM + assert r1["type"] is FlowResultType.FORM assert r1["step_id"] == SOURCE_USER assert "flow_id" in r1 result1 = await hass.config_entries.flow.async_configure( r1["flow_id"], user_input={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result1["type"] == FlowResultType.CREATE_ENTRY + assert result1["type"] is FlowResultType.CREATE_ENTRY r2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert r2["type"] == FlowResultType.FORM + assert r2["type"] is FlowResultType.FORM assert r2["step_id"] == SOURCE_USER assert "flow_id" in r2 result2 = await hass.config_entries.flow.async_configure( r2["flow_id"], user_input={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index e4770343114..6a758ec5066 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.hlk_sw16.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType class MockSW16Client: @@ -32,9 +33,8 @@ class MockSW16Client: if self.disconnect_callback: self.disconnect_callback() return await self.active_transaction - else: - self.active_transaction.set_result(True) - return self.active_transaction + self.active_transaction.set_result(True) + return self.active_transaction def stop(self): """Mock client stop.""" @@ -55,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} conf = { @@ -84,7 +84,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1:8080" assert result2["data"] == { "host": "127.0.0.1", @@ -102,7 +102,7 @@ async def test_form(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {} result4 = await hass.config_entries.flow.async_configure( @@ -110,7 +110,7 @@ async def test_form(hass: HomeAssistant) -> None: conf, ) - assert result4["type"] == "abort" + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -120,7 +120,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} conf = { @@ -149,7 +149,7 @@ async def test_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1:8080" assert result2["data"] == { "host": "127.0.0.1", @@ -181,7 +181,7 @@ async def test_form_invalid_data(hass: HomeAssistant) -> None: conf, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -206,5 +206,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: conf, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/holiday/test_config_flow.py b/tests/components/holiday/test_config_flow.py index 44a72f58404..14e2b68234c 100644 --- a/tests/components/holiday/test_config_flow.py +++ b/tests/components/holiday/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Germany, BW" assert result3["data"] == { "country": "DE", @@ -54,7 +54,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -64,7 +64,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sweden" assert result2["data"] == { "country": "SE", @@ -108,7 +108,7 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=data_se, ) - assert result_se["type"] == FlowResultType.ABORT + assert result_se["type"] is FlowResultType.ABORT assert result_se["reason"] == "already_configured" # Test for country with subdivisions @@ -117,7 +117,7 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=data_de, ) - assert result_de_step1["type"] == FlowResultType.FORM + assert result_de_step1["type"] is FlowResultType.FORM result_de_step2 = await hass.config_entries.flow.async_configure( result_de_step1["flow_id"], @@ -125,7 +125,7 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: CONF_PROVINCE: data_de[CONF_PROVINCE], }, ) - assert result_de_step2["type"] == FlowResultType.ABORT + assert result_de_step2["type"] is FlowResultType.ABORT assert result_de_step2["reason"] == "already_configured" @@ -167,7 +167,7 @@ async def test_form_babel_unresolved_language(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Germany, BW" assert result["data"] == { "country": "DE", @@ -213,7 +213,7 @@ async def test_form_babel_replace_dash_with_underscore(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Germany, BW" assert result["data"] == { "country": "DE", @@ -237,7 +237,7 @@ async def test_reconfigure(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -247,7 +247,7 @@ async def test_reconfigure(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Germany, NW" @@ -274,7 +274,7 @@ async def test_reconfigure_incorrect_language( "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -284,7 +284,7 @@ async def test_reconfigure_incorrect_language( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Germany, NW" @@ -315,7 +315,7 @@ async def test_reconfigure_entry_exists( "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -325,7 +325,7 @@ async def test_reconfigure_entry_exists( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Germany, BW" diff --git a/tests/components/holiday/test_init.py b/tests/components/holiday/test_init.py index a044e390a68..38eb51fe925 100644 --- a/tests/components/holiday/test_init.py +++ b/tests/components/holiday/test_init.py @@ -20,10 +20,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() state: ConfigEntryState = entry.state - assert state == ConfigEntryState.NOT_LOADED + assert state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py new file mode 100644 index 00000000000..5107fb44d69 --- /dev/null +++ b/tests/components/home_connect/conftest.py @@ -0,0 +1,235 @@ +"""Test fixtures for home_connect.""" + +from collections.abc import Awaitable, Callable, Generator +import time +from typing import Any +from unittest.mock import MagicMock, Mock, PropertyMock, patch + +from homeconnect.api import HomeConnectAppliance, HomeConnectError +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.home_connect import update_all_devices +from homeassistant.components.home_connect.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_json_object_fixture + +MOCK_APPLIANCES_PROPERTIES = { + x["name"]: x + for x in load_json_object_fixture("home_connect/appliances.json")["data"][ + "homeappliances" + ] +} + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +FAKE_ACCESS_TOKEN = "some-access-token" +FAKE_REFRESH_TOKEN = "some-refresh-token" +FAKE_AUTH_IMPL = "conftest-imported-cred" + +SERVER_ACCESS_TOKEN = { + "refresh_token": "server-refresh-token", + "access_token": "server-access-token", + "type": "Bearer", + "expires_in": 60, +} + + +@pytest.fixture(name="token_expiration_time") +def mock_token_expiration_time() -> float: + """Fixture for expiration time of the config entry auth token.""" + return time.time() + 86400 + + +@pytest.fixture(name="token_entry") +def mock_token_entry(token_expiration_time: float) -> dict[str, Any]: + """Fixture for OAuth 'token' data for a ConfigEntry.""" + return { + "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": FAKE_ACCESS_TOKEN, + "type": "Bearer", + "expires_at": token_expiration_time, + } + + +@pytest.fixture(name="config_entry") +def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry: + """Fixture for a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": FAKE_AUTH_IMPL, + "token": token_entry, + }, + ) + + +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + FAKE_AUTH_IMPL, + ) + + +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to specify platforms to test.""" + return [] + + +async def bypass_throttle(hass: HomeAssistant, config_entry: MockConfigEntry): + """Add kwarg to disable throttle.""" + await update_all_devices(hass, config_entry, no_throttle=True) + + +@pytest.fixture(name="bypass_throttle") +def mock_bypass_throttle(): + """Fixture to bypass the throttle decorator in __init__.""" + with patch( + "homeassistant.components.home_connect.update_all_devices", + side_effect=lambda x, y: bypass_throttle(x, y), + ): + yield + + +@pytest.fixture(name="integration_setup") +async def mock_integration_setup( + hass: HomeAssistant, + platforms: list[Platform], + config_entry: MockConfigEntry, +) -> Callable[[], Awaitable[bool]]: + """Fixture to set up the integration.""" + config_entry.add_to_hass(hass) + + async def run() -> bool: + with patch("homeassistant.components.home_connect.PLATFORMS", platforms): + result = await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return result + + return run + + +@pytest.fixture(name="get_appliances") +def mock_get_appliances() -> Generator[None, Any, None]: + """Mock ConfigEntryAuth parent (HomeAssistantAPI) method.""" + with patch( + "homeassistant.components.home_connect.api.ConfigEntryAuth.get_appliances", + ) as mock: + yield mock + + +@pytest.fixture(name="appliance") +def mock_appliance(request) -> Mock: + """Fixture to mock Appliance.""" + app = "Washer" + if hasattr(request, "param") and request.param: + app = request.param + + mock = MagicMock( + autospec=HomeConnectAppliance, + **MOCK_APPLIANCES_PROPERTIES.get(app), + ) + mock.name = app + type(mock).status = PropertyMock(return_value={}) + mock.get.return_value = {} + mock.get_programs_available.return_value = [] + mock.get_status.return_value = {} + mock.get_settings.return_value = {} + + return mock + + +@pytest.fixture(name="problematic_appliance") +def mock_problematic_appliance() -> Mock: + """Fixture to mock a problematic Appliance.""" + app = "Washer" + mock = Mock( + spec=HomeConnectAppliance, + **MOCK_APPLIANCES_PROPERTIES.get(app), + ) + mock.name = app + setattr(mock, "status", {}) + mock.get_programs_active.side_effect = HomeConnectError + mock.get_programs_available.side_effect = HomeConnectError + mock.start_program.side_effect = HomeConnectError + mock.stop_program.side_effect = HomeConnectError + mock.get_status.side_effect = HomeConnectError + mock.get_settings.side_effect = HomeConnectError + mock.set_setting.side_effect = HomeConnectError + + return mock + + +def get_all_appliances(): + """Return a list of `HomeConnectAppliance` instances for all appliances.""" + + appliances = {} + + data = load_json_object_fixture("home_connect/appliances.json").get("data") + programs_active = load_json_object_fixture("home_connect/programs-active.json") + programs_available = load_json_object_fixture( + "home_connect/programs-available.json" + ) + + def listen_callback(mock, callback): + callback["callback"](mock) + + for home_appliance in data["homeappliances"]: + api_status = load_json_object_fixture("home_connect/status.json") + api_settings = load_json_object_fixture("home_connect/settings.json") + + ha_id = home_appliance["haId"] + ha_type = home_appliance["type"] + + appliance = MagicMock(spec=HomeConnectAppliance, **home_appliance) + appliance.name = home_appliance["name"] + appliance.listen_events.side_effect = ( + lambda app=appliance, **x: listen_callback(app, x) + ) + appliance.get_programs_active.return_value = programs_active.get( + ha_type, {} + ).get("data", {}) + appliance.get_programs_available.return_value = [ + program["key"] + for program in programs_available.get(ha_type, {}) + .get("data", {}) + .get("programs", []) + ] + appliance.get_status.return_value = HomeConnectAppliance.json2dict( + api_status.get("data", {}).get("status", []) + ) + appliance.get_settings.return_value = HomeConnectAppliance.json2dict( + api_settings.get(ha_type, {}).get("data", {}).get("settings", []) + ) + setattr(appliance, "status", {}) + appliance.status.update(appliance.get_status.return_value) + appliance.status.update(appliance.get_settings.return_value) + appliance.set_setting.side_effect = ( + lambda x, y, appliance=appliance: appliance.status.update({x: {"value": y}}) + ) + appliance.start_program.side_effect = ( + lambda x, appliance=appliance: appliance.status.update( + {"BSH.Common.Root.ActiveProgram": {"value": x}} + ) + ) + appliance.stop_program.side_effect = ( + lambda appliance=appliance: appliance.status.update( + {"BSH.Common.Root.ActiveProgram": {}} + ) + ) + + appliances[ha_id] = appliance + + return list(appliances.values()) diff --git a/tests/components/home_connect/fixtures/appliances.json b/tests/components/home_connect/fixtures/appliances.json new file mode 100644 index 00000000000..ada18b3482c --- /dev/null +++ b/tests/components/home_connect/fixtures/appliances.json @@ -0,0 +1,123 @@ +{ + "data": { + "homeappliances": [ + { + "name": "FridgeFreezer", + "brand": "SIEMENS", + "vib": "HCS05FRF1", + "connected": true, + "type": "FridgeFreezer", + "enumber": "HCS05FRF1/03", + "haId": "SIEMENS-HCS05FRF1-304F4F9E541D" + }, + { + "name": "Dishwasher", + "brand": "SIEMENS", + "vib": "HCS02DWH1", + "connected": true, + "type": "Dishwasher", + "enumber": "HCS02DWH1/03", + "haId": "SIEMENS-HCS02DWH1-6BE58C26DCC1" + }, + { + "name": "Oven", + "brand": "BOSCH", + "vib": "HCS01OVN1", + "connected": true, + "type": "Oven", + "enumber": "HCS01OVN1/03", + "haId": "BOSCH-HCS01OVN1-43E0065FE245" + }, + { + "name": "Washer", + "brand": "SIEMENS", + "vib": "HCS03WCH1", + "connected": true, + "type": "Washer", + "enumber": "HCS03WCH1/03", + "haId": "SIEMENS-HCS03WCH1-7BC6383CF794" + }, + { + "name": "Dryer", + "brand": "BOSCH", + "vib": "HCS04DYR1", + "connected": true, + "type": "Dryer", + "enumber": "HCS04DYR1/03", + "haId": "BOSCH-HCS04DYR1-831694AE3C5A" + }, + { + "name": "CoffeeMaker", + "brand": "BOSCH", + "vib": "HCS06COM1", + "connected": true, + "type": "CoffeeMaker", + "enumber": "HCS06COM1/03", + "haId": "BOSCH-HCS06COM1-D70390681C2C" + }, + { + "name": "WasherDryer", + "brand": "BOSCH", + "vib": "HCS000001", + "connected": true, + "type": "WasherDryer", + "enumber": "HCS000000/01", + "haId": "BOSCH-HCS000000-D00000000001" + }, + { + "name": "Refrigerator", + "brand": "BOSCH", + "vib": "HCS000002", + "connected": true, + "type": "Refrigerator", + "enumber": "HCS000000/02", + "haId": "BOSCH-HCS000000-D00000000002" + }, + { + "name": "Freezer", + "brand": "BOSCH", + "vib": "HCS000003", + "connected": true, + "type": "Freezer", + "enumber": "HCS000000/03", + "haId": "BOSCH-HCS000000-D00000000003" + }, + { + "name": "Hood", + "brand": "BOSCH", + "vib": "HCS000004", + "connected": true, + "type": "Hood", + "enumber": "HCS000000/04", + "haId": "BOSCH-HCS000000-D00000000004" + }, + { + "name": "Hob", + "brand": "BOSCH", + "vib": "HCS000005", + "connected": true, + "type": "Hob", + "enumber": "HCS000000/05", + "haId": "BOSCH-HCS000000-D00000000005" + }, + { + "name": "CookProcessor", + "brand": "BOSCH", + "vib": "HCS000006", + "connected": true, + "type": "CookProcessor", + "enumber": "HCS000000/06", + "haId": "BOSCH-HCS000000-D00000000006" + }, + { + "name": "DNE", + "brand": "BOSCH", + "vib": "HCS000000", + "connected": true, + "type": "DNE", + "enumber": "HCS000000/00", + "haId": "BOSCH-000000000-000000000000" + } + ] + } +} diff --git a/tests/components/home_connect/fixtures/programs-active.json b/tests/components/home_connect/fixtures/programs-active.json new file mode 100644 index 00000000000..32356a81275 --- /dev/null +++ b/tests/components/home_connect/fixtures/programs-active.json @@ -0,0 +1,28 @@ +{ + "Oven": { + "data": { + "key": "Cooking.Oven.Program.HeatingMode.HotAir", + "name": "Hot air", + "options": [ + { + "key": "Cooking.Oven.Option.SetpointTemperature", + "name": "Target temperature for the cavity", + "value": 230, + "unit": "°C" + }, + { + "key": "BSH.Common.Option.Duration", + "name": "Adjust the duration", + "value": 1200, + "unit": "seconds" + } + ] + } + }, + "Washer": { + "data": { + "key": "BSH.Common.Root.ActiveProgram", + "value": "LaundryCare.Dryer.Program.Mix" + } + } +} diff --git a/tests/components/home_connect/fixtures/programs-available.json b/tests/components/home_connect/fixtures/programs-available.json new file mode 100644 index 00000000000..b99ee5c6add --- /dev/null +++ b/tests/components/home_connect/fixtures/programs-available.json @@ -0,0 +1,185 @@ +{ + "Oven": { + "data": { + "programs": [ + { + "key": "Cooking.Oven.Program.HeatingMode.HotAir", + "name": "Hot air", + "contraints": { + "execution": "selectandstart" + } + }, + { + "key": "Cooking.Oven.Program.HeatingMode.TopBottomHeating", + "name": "Top/bottom heating", + "contraints": { + "execution": "none" + } + }, + { + "key": "Cooking.Oven.Program.HeatingMode.PizzaSetting", + "name": "Pizza setting", + "contraints": { + "execution": "startonly" + } + } + ] + } + }, + "DishWasher": { + "data": { + "programs": [ + { + "key": "Dishcare.Dishwasher.Program.Auto1", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Auto2", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Auto3", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Eco50", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Quick45", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "Washer": { + "data": { + "programs": [ + { + "key": "LaundryCare.Washer.Program.Cotton", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.EasyCare", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.Mix", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.DelicatesSilk", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.Wool", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "Dryer": { + "data": { + "programs": [ + { + "key": "LaundryCare.Dryer.Program.Cotton", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Dryer.Program.Synthetic", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Dryer.Program.Mix", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "CoffeeMaker": { + "data": { + "programs": [ + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "WasherDryer": { + "data": { + "programs": [ + { + "key": "LaundryCare.WasherDryer.Program.Mix", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Option.Temperature", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + } +} diff --git a/tests/components/home_connect/fixtures/settings.json b/tests/components/home_connect/fixtures/settings.json new file mode 100644 index 00000000000..5dc0f0e0599 --- /dev/null +++ b/tests/components/home_connect/fixtures/settings.json @@ -0,0 +1,99 @@ +{ + "Dishwasher": { + "data": { + "settings": [ + { + "key": "BSH.Common.Setting.AmbientLightEnabled", + "value": true, + "type": "Boolean" + }, + { + "key": "BSH.Common.Setting.AmbientLightBrightness", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "BSH.Common.Setting.AmbientLightColor", + "value": "BSH.Common.EnumType.AmbientLightColor.Color43", + "type": "BSH.Common.EnumType.AmbientLightColor" + }, + { + "key": "BSH.Common.Setting.AmbientLightCustomColor", + "value": "#4a88f8", + "type": "String" + }, + { + "key": "BSH.Common.Setting.PowerState", + "value": "BSH.Common.EnumType.PowerState.On", + "type": "BSH.Common.EnumType.PowerState" + }, + { + "key": "BSH.Common.Setting.ChildLock", + "value": false, + "type": "Boolean" + } + ] + } + }, + "Hood": { + "data": { + "settings": [ + { + "key": "Cooking.Common.Setting.Lighting", + "value": true, + "type": "Boolean" + }, + { + "key": "Cooking.Common.Setting.LightingBrightness", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "Cooking.Hood.Setting.ColorTemperaturePercent", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "BSH.Common.Setting.ColorTemperature", + "value": "Cooking.Hood.EnumType.ColorTemperature.warmToNeutral", + "type": "BSH.Common.EnumType.ColorTemperature" + }, + { + "key": "BSH.Common.Setting.AmbientLightEnabled", + "value": true, + "type": "Boolean" + }, + { + "key": "BSH.Common.Setting.AmbientLightBrightness", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "BSH.Common.Setting.AmbientLightColor", + "value": "BSH.Common.EnumType.AmbientLightColor.Color43", + "type": "BSH.Common.EnumType.AmbientLightColor" + }, + { + "key": "BSH.Common.Setting.AmbientLightCustomColor", + "value": "#4a88f8", + "type": "String" + } + ] + } + }, + "Oven": { + "data": { + "settings": [ + { + "key": "BSH.Common.Setting.PowerState", + "value": "BSH.Common.EnumType.PowerState.On", + "type": "BSH.Common.EnumType.PowerState" + } + ] + } + } +} diff --git a/tests/components/home_connect/fixtures/status.json b/tests/components/home_connect/fixtures/status.json new file mode 100644 index 00000000000..8eac586a308 --- /dev/null +++ b/tests/components/home_connect/fixtures/status.json @@ -0,0 +1,16 @@ +{ + "data": { + "status": [ + { "key": "BSH.Common.Status.RemoteControlActive", "value": true }, + { "key": "BSH.Common.Status.RemoteControlStartAllowed", "value": true }, + { + "key": "BSH.Common.Status.OperationState", + "value": "BSH.Common.EnumType.OperationState.Ready" + }, + { + "key": "BSH.Common.Status.DoorState", + "value": "BSH.Common.EnumType.DoorState.Closed" + } + ] + } +} diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 74ca918889d..2c094c74246 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -3,7 +3,7 @@ from http import HTTPStatus from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, @@ -14,6 +14,7 @@ from homeassistant.components.home_connect.const import ( OAUTH2_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.test_util.aiohttp import AiohttpClientMocker @@ -47,7 +48,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py new file mode 100644 index 00000000000..e304e2947d5 --- /dev/null +++ b/tests/components/home_connect/test_init.py @@ -0,0 +1,301 @@ +"""Test the integration init functionality.""" + +from collections.abc import Awaitable, Callable, Generator +from typing import Any +from unittest.mock import MagicMock, Mock + +import pytest +from requests import HTTPError +import requests_mock + +from homeassistant.components.home_connect.const import DOMAIN, OAUTH2_TOKEN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import ( + CLIENT_ID, + CLIENT_SECRET, + FAKE_ACCESS_TOKEN, + FAKE_REFRESH_TOKEN, + SERVER_ACCESS_TOKEN, + get_all_appliances, +) + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +SERVICE_KV_CALL_PARAMS = [ + { + "domain": DOMAIN, + "service": "set_option_active", + "service_data": { + "device_id": "DEVICE_ID", + "key": "", + "value": "", + "unit": "", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "set_option_selected", + "service_data": { + "device_id": "DEVICE_ID", + "key": "", + "value": "", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "change_setting", + "service_data": { + "device_id": "DEVICE_ID", + "key": "", + "value": "", + }, + "blocking": True, + }, +] + +SERVICE_COMMAND_CALL_PARAMS = [ + { + "domain": DOMAIN, + "service": "pause_program", + "service_data": { + "device_id": "DEVICE_ID", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "resume_program", + "service_data": { + "device_id": "DEVICE_ID", + }, + "blocking": True, + }, +] + + +SERVICE_PROGRAM_CALL_PARAMS = [ + { + "domain": DOMAIN, + "service": "select_program", + "service_data": { + "device_id": "DEVICE_ID", + "program": "", + "key": "", + "value": "", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "start_program", + "service_data": { + "device_id": "DEVICE_ID", + "program": "", + "key": "", + "value": "", + "unit": "C", + }, + "blocking": True, + }, +] + +SERVICE_APPLIANCE_METHOD_MAPPING = { + "set_option_active": "set_options_active_program", + "set_option_selected": "set_options_selected_program", + "change_setting": "set_setting", + "pause_program": "execute_command", + "resume_program": "execute_command", + "select_program": "select_program", + "start_program": "start_program", +} + + +async def test_api_setup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Test setup and unload.""" + get_appliances.side_effect = get_all_appliances + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_exception_handling( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + integration_setup: Callable[[], Awaitable[bool]], + config_entry: MockConfigEntry, + setup_credentials: None, + get_appliances: MagicMock, + problematic_appliance: Mock, +) -> None: + """Test exception handling.""" + get_appliances.return_value = [problematic_appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + +@pytest.mark.parametrize("token_expiration_time", [12345]) +async def test_token_refresh_success( + bypass_throttle: Generator[None, Any, None], + integration_setup: Callable[[], Awaitable[bool]], + config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, + requests_mock: requests_mock.Mocker, + setup_credentials: None, +) -> None: + """Test where token is expired and the refresh attempt succeeds.""" + + assert config_entry.data["token"]["access_token"] == FAKE_ACCESS_TOKEN + + requests_mock.post(OAUTH2_TOKEN, json=SERVER_ACCESS_TOKEN) + requests_mock.get("/api/homeappliances", json={"data": {"homeappliances": []}}) + + aioclient_mock.post( + OAUTH2_TOKEN, + json=SERVER_ACCESS_TOKEN, + ) + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + # Verify token request + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][2] == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": FAKE_REFRESH_TOKEN, + } + + # Verify updated token + assert ( + config_entry.data["token"]["access_token"] + == SERVER_ACCESS_TOKEN["access_token"] + ) + + +async def test_setup( + hass: HomeAssistant, + integration_setup: Callable[[], Awaitable[bool]], + config_entry: MockConfigEntry, + setup_credentials: None, +) -> None: + """Test setting up the integration.""" + assert config_entry.state == ConfigEntryState.NOT_LOADED + + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_update_throttle( + appliance: Mock, + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + platforms: list[Platform], + get_appliances: MagicMock, +) -> None: + """Test to check Throttle functionality.""" + assert config_entry.state == ConfigEntryState.NOT_LOADED + + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + assert get_appliances.call_count == 0 + + +async def test_http_error( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Test HTTP errors during setup integration.""" + get_appliances.side_effect = HTTPError(response=MagicMock()) + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + assert get_appliances.call_count == 1 + + +@pytest.mark.parametrize( + "service_call", + SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, +) +async def test_services( + service_call: list[dict[str, Any]], + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, + appliance: Mock, +) -> None: + """Create and test services.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, appliance.haId)}, + ) + + service_name = service_call["service"] + service_call["service_data"]["device_id"] = device_entry.id + await hass.services.async_call(**service_call) + await hass.async_block_till_done() + assert ( + getattr(appliance, SERVICE_APPLIANCE_METHOD_MAPPING[service_name]).call_count + == 1 + ) + + +async def test_services_exception( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, + appliance: Mock, +) -> None: + """Raise a ValueError when device id does not match.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + service_call = SERVICE_KV_CALL_PARAMS[0] + + with pytest.raises(ValueError): + service_call["service_data"]["device_id"] = "DOES_NOT_EXISTS" + await hass.services.async_call(**service_call) + await hass.async_block_till diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py new file mode 100644 index 00000000000..f30f017d6d3 --- /dev/null +++ b/tests/components/home_connect/test_sensor.py @@ -0,0 +1,211 @@ +"""Tests for home_connect sensor entities.""" + +from collections.abc import Awaitable, Callable, Generator +from typing import Any +from unittest.mock import MagicMock, Mock + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity + +from tests.common import MockConfigEntry + +TEST_HC_APP = "Dishwasher" + + +EVENT_PROG_DELAYED_START = { + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Delayed" + }, +} + +EVENT_PROG_REMAIN_NO_VALUE = { + "BSH.Common.Option.RemainingProgramTime": {}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Delayed" + }, +} + + +EVENT_PROG_RUN = { + "BSH.Common.Option.RemainingProgramTime": {"value": "0"}, + "BSH.Common.Option.ProgramProgress": {"value": "60"}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Run" + }, +} + + +EVENT_PROG_UPDATE_1 = { + "BSH.Common.Option.RemainingProgramTime": {"value": "0"}, + "BSH.Common.Option.ProgramProgress": {"value": "80"}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Run" + }, +} + +EVENT_PROG_UPDATE_2 = { + "BSH.Common.Option.RemainingProgramTime": {"value": "20"}, + "BSH.Common.Option.ProgramProgress": {"value": "99"}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Run" + }, +} + +EVENT_PROG_END = { + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Ready" + }, +} + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.SENSOR] + + +async def test_sensors( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, + appliance: Mock, +) -> None: + """Test sensor entities.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + +# Appliance program sequence with a delayed start. +PROGRAM_SEQUENCE_EVENTS = ( + EVENT_PROG_DELAYED_START, + EVENT_PROG_RUN, + EVENT_PROG_UPDATE_1, + EVENT_PROG_UPDATE_2, + EVENT_PROG_END, +) + +# Entity mapping to expected state at each program sequence. +ENTITY_ID_STATES = { + "sensor.dishwasher_operation_state": ( + "Delayed", + "Run", + "Run", + "Run", + "Ready", + ), + "sensor.dishwasher_remaining_program_time": ( + "unavailable", + "2021-01-09T12:00:00+00:00", + "2021-01-09T12:00:00+00:00", + "2021-01-09T12:00:20+00:00", + "unavailable", + ), + "sensor.dishwasher_program_progress": ( + "unavailable", + "60", + "80", + "99", + "unavailable", + ), +} + + +@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) +@pytest.mark.parametrize( + ("states", "event_run"), + list( + zip( + list(zip(*ENTITY_ID_STATES.values(), strict=False)), + PROGRAM_SEQUENCE_EVENTS, + strict=False, + ) + ), +) +async def test_event_sensors( + appliance: Mock, + states: tuple, + event_run: dict, + freezer: FrozenDateTimeFactory, + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Test sequence for sensors that are only available after an event happens.""" + entity_ids = ENTITY_ID_STATES.keys() + + time_to_freeze = "2021-01-09 12:00:00+00:00" + freezer.move_to(time_to_freeze) + + get_appliances.return_value = [appliance] + + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + appliance.status.update(event_run) + for entity_id, state in zip(entity_ids, states, strict=False): + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + assert hass.states.is_state(entity_id, state) + + +# Program sequence for SensorDeviceClass.TIMESTAMP edge cases. +PROGRAM_SEQUENCE_EDGE_CASE = [ + EVENT_PROG_REMAIN_NO_VALUE, + EVENT_PROG_RUN, + EVENT_PROG_END, + EVENT_PROG_END, +] + +# Expected state at each sequence. +ENTITY_ID_EDGE_CASE_STATES = [ + "unavailable", + "2021-01-09T12:00:01+00:00", + "unavailable", + "unavailable", +] + + +@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) +async def test_remaining_prog_time_edge_cases( + appliance: Mock, + freezer: FrozenDateTimeFactory, + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Run program sequence to test edge cases for the remaining_prog_time entity.""" + get_appliances.return_value = [appliance] + entity_id = "sensor.dishwasher_remaining_program_time" + time_to_freeze = "2021-01-09 12:00:00+00:00" + freezer.move_to(time_to_freeze) + + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + for ( + event, + expected_state, + ) in zip(PROGRAM_SEQUENCE_EDGE_CASE, ENTITY_ID_EDGE_CASE_STATES, strict=False): + appliance.status.update(event) + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + freezer.tick() + assert hass.states.is_state(entity_id, expected_state) diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index e20fcb69d00..9a14198b1ef 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -32,10 +32,9 @@ def entities_fixture( """Set up the test environment.""" if request.param == "entities_unique_id": return entities_unique_id(entity_registry) - elif request.param == "entities_no_unique_id": + if request.param == "entities_no_unique_id": return entities_no_unique_id(hass) - else: - raise RuntimeError("Invalid setup fixture") + raise RuntimeError("Invalid setup fixture") def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]: diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index a0c1f6cb45d..451f35f66fe 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index ebe90415018..2afb533cdc0 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 65c2863d0d7..2e2dca5b57a 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -8,7 +8,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) @@ -980,16 +980,13 @@ async def test_template_string(hass: HomeAssistant, calls, below) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "below", - "above", - "from_state.state", - "to_state.state", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.below }}" + " - {{ trigger.above }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" ) }, }, @@ -1346,9 +1343,10 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls, above) -> { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "entity_id", "to_state.state") + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.to_state.state }}" ) }, }, diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 9d1d60031e0..597ef0ab1a5 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -6,7 +6,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( ATTR_ENTITY_ID, @@ -55,16 +55,13 @@ async def test_if_fires_on_entity_change(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" + " - {{ trigger.id }}" ) }, }, @@ -114,16 +111,13 @@ async def test_if_fires_on_entity_change_uuid( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" + " - {{ trigger.id }}" ) }, }, @@ -1079,14 +1073,11 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls) -> None: { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" ) }, }, diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 2d814813ed4..2324599c3c6 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -6,8 +6,8 @@ from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol -import homeassistant.components.automation as automation -import homeassistant.components.homeassistant.triggers.time_pattern as time_pattern +from homeassistant.components import automation +from homeassistant.components.homeassistant.triggers import time_pattern from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py index f0bf15aaa53..5fa71d9a091 100644 --- a/tests/components/homeassistant_green/test_config_flow.py +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -49,7 +49,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Green" assert result["data"] == {} assert result["options"] == {} @@ -83,7 +83,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() @@ -109,7 +109,7 @@ async def test_option_flow_non_hassio( ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_hassio" @@ -132,14 +132,14 @@ async def test_option_flow_led_settings( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hardware_settings" result = await hass.config_entries.options.async_configure( result["flow_id"], {"activity_led": False, "power_led": False, "system_health_led": False}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY set_green_settings.assert_called_once_with( hass, {"activity_led": False, "power_led": False, "system_health_led": False} ) @@ -164,14 +164,14 @@ async def test_option_flow_led_settings_unchanged( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hardware_settings" result = await hass.config_entries.options.async_configure( result["flow_id"], {"activity_led": True, "power_led": True, "system_health_led": True}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY set_green_settings.assert_not_called() @@ -195,7 +195,7 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "read_hw_settings_error" @@ -216,7 +216,7 @@ async def test_option_flow_led_settings_fail_2( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hardware_settings" with patch( @@ -227,5 +227,5 @@ async def test_option_flow_led_settings_fail_2( result["flow_id"], {"activity_led": False, "power_led": False, "system_health_led": False}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "write_hw_settings_error" diff --git a/tests/components/homeassistant_green/test_init.py b/tests/components/homeassistant_green/test_init.py index 0efb449137a..44cd6da136a 100644 --- a/tests/components/homeassistant_green/test_init.py +++ b/tests/components/homeassistant_green/test_init.py @@ -105,4 +105,4 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 82b6fd0c092..d04f725baf6 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -230,7 +230,7 @@ async def test_option_flow_install_multi_pan_addon( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -239,7 +239,7 @@ async def test_option_flow_install_multi_pan_addon( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -247,7 +247,7 @@ async def test_option_flow_install_multi_pan_addon( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -266,7 +266,7 @@ async def test_option_flow_install_multi_pan_addon( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_zha( @@ -305,7 +305,7 @@ async def test_option_flow_install_multi_pan_addon_zha( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -314,7 +314,7 @@ async def test_option_flow_install_multi_pan_addon_zha( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -330,7 +330,7 @@ async def test_option_flow_install_multi_pan_addon_zha( return_value=11, ): result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -361,7 +361,7 @@ async def test_option_flow_install_multi_pan_addon_zha( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_zha_other_radio( @@ -400,7 +400,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -409,7 +409,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -418,7 +418,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( addon_info.return_value["hostname"] = "core-silabs-multiprotocol" result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -437,7 +437,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Check the ZHA entry data is not changed assert zha_config_entry.data == { @@ -469,7 +469,7 @@ async def test_option_flow_non_hassio( ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_hassio" @@ -490,11 +490,11 @@ async def test_option_flow_addon_installed_other_device( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_installed_other_device" result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -528,30 +528,30 @@ async def test_option_flow_addon_installed_same_device_reconfigure_unexpected_us config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "reconfigure_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "notify_unknown_multipan_user" result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "change_channel" assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel result = await hass.config_entries.options.async_configure( result["flow_id"], {"channel": "14"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "notify_channel_change" assert result["description_placeholders"] == {"delay_minutes": "5"} result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_multiprotocol_platform.change_channel_calls == [(14, 300)] assert multipan_manager._channel == 14 @@ -601,26 +601,26 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "reconfigure_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "change_channel" assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel result = await hass.config_entries.options.async_configure( result["flow_id"], {"channel": "14"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "notify_channel_change" assert result["description_placeholders"] == {"delay_minutes": "5"} result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY for domain in ["otbr", "zha"]: assert mock_multiprotocol_platforms[domain].change_channel_calls == [(14, 300)] @@ -664,14 +664,14 @@ async def test_option_flow_addon_installed_same_device_uninstall( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" # Make sure the flasher addon is installed @@ -685,14 +685,14 @@ async def test_option_flow_addon_installed_same_device_uninstall( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_flasher_addon" assert result["progress_action"] == "install_addon" await hass.async_block_till_done() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "uninstall_multiprotocol_addon" assert result["progress_action"] == "uninstall_multiprotocol_addon" @@ -700,7 +700,7 @@ async def test_option_flow_addon_installed_same_device_uninstall( uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -709,7 +709,7 @@ async def test_option_flow_addon_installed_same_device_uninstall( install_addon.assert_called_once_with(hass, "core_silabs_flasher") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Check the ZHA config entry data is updated assert zha_config_entry.data == { @@ -748,21 +748,21 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: False} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_flasher_already_running_failure( @@ -791,14 +791,14 @@ async def test_option_flow_flasher_already_running_failure( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" # The flasher addon is already installed and running, this is bad @@ -808,7 +808,7 @@ async def test_option_flow_flasher_already_running_failure( result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_already_running" @@ -838,14 +838,14 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" addon_store_info.return_value = { @@ -857,7 +857,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "uninstall_multiprotocol_addon" assert result["progress_action"] == "uninstall_multiprotocol_addon" @@ -865,7 +865,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -879,7 +879,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed install_addon.assert_not_called() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_flasher_install_failure( @@ -919,14 +919,14 @@ async def test_option_flow_flasher_install_failure( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" addon_store_info.return_value = { @@ -939,7 +939,7 @@ async def test_option_flow_flasher_install_failure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_flasher_addon" assert result["progress_action"] == "install_addon" @@ -947,7 +947,7 @@ async def test_option_flow_flasher_install_failure( install_addon.assert_called_once_with(hass, "core_silabs_flasher") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -977,20 +977,20 @@ async def test_option_flow_flasher_addon_flash_failure( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "uninstall_multiprotocol_addon" assert result["progress_action"] == "uninstall_multiprotocol_addon" @@ -1000,7 +1000,7 @@ async def test_option_flow_flasher_addon_flash_failure( uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -1008,7 +1008,7 @@ async def test_option_flow_flasher_addon_flash_failure( await hass.async_block_till_done() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" assert result["description_placeholders"]["addon_name"] == "Silicon Labs Flasher" @@ -1055,21 +1055,21 @@ async def test_option_flow_uninstall_migration_initiate_failure( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" mock_initiate_migration.assert_called_once() @@ -1116,14 +1116,14 @@ async def test_option_flow_uninstall_migration_finish_failure( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( @@ -1134,7 +1134,7 @@ async def test_option_flow_uninstall_migration_finish_failure( uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -1142,7 +1142,7 @@ async def test_option_flow_uninstall_migration_finish_failure( await hass.async_block_till_done() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" @@ -1163,7 +1163,7 @@ async def test_option_flow_do_not_install_multi_pan_addon( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1172,7 +1172,7 @@ async def test_option_flow_do_not_install_multi_pan_addon( "enable_multi_pan": False, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_install_fails( @@ -1197,7 +1197,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1206,7 +1206,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1214,7 +1214,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -1240,7 +1240,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1249,7 +1249,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1257,7 +1257,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -1276,7 +1276,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -1302,7 +1302,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1311,7 +1311,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1319,7 +1319,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_set_config_failed" @@ -1342,7 +1342,7 @@ async def test_option_flow_addon_info_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_info_failed" @@ -1379,7 +1379,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1388,7 +1388,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1396,7 +1396,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" set_addon_options.assert_not_called() @@ -1435,7 +1435,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1444,7 +1444,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1452,7 +1452,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -1471,7 +1471,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 957a407cc0e..611dda4a917 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,23 +1,33 @@ """Test the Home Assistant SkyConnect config flow.""" -from collections.abc import Generator -import copy -from unittest.mock import Mock, patch +import asyncio +from collections.abc import Awaitable, Callable +from typing import Any +from unittest.mock import AsyncMock, Mock, call, patch import pytest +from universal_silabs_flasher.const import ApplicationType -from homeassistant.components import homeassistant_sky_connect, usb +from homeassistant.components import usb +from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + CONF_DISABLE_MULTI_PAN, + get_flasher_addon_manager, + get_multiprotocol_addon_manager, +) +from homeassistant.components.homeassistant_sky_connect.config_flow import ( + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, +) from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.components.zha import ( - CONF_DEVICE_PATH, - DOMAIN as ZHA_DOMAIN, - RadioType, +from homeassistant.components.homeassistant_sky_connect.util import ( + get_otbr_addon_manager, + get_zigbee_flasher_addon_manager, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.common import MockConfigEntry USB_DATA_SKY = usb.UsbServiceInfo( device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", @@ -38,340 +48,876 @@ USB_DATA_ZBT1 = usb.UsbServiceInfo( ) -@pytest.fixture(autouse=True) -def config_flow_handler(hass: HomeAssistant) -> Generator[None, None, None]: - """Fixture for a test config flow.""" - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" - ): - yield +def delayed_side_effect() -> Callable[..., Awaitable[None]]: + """Slows down eager tasks by delaying for an event loop tick.""" + + async def side_effect(*args: Any, **kwargs: Any) -> None: + await asyncio.sleep(0) + + return side_effect @pytest.mark.parametrize( - ("usb_data", "title"), + ("usb_data", "model"), [ (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 +async def test_config_flow_zigbee( + usb_data: usb.UsbServiceInfo, model: 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 - ) - - 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, - } - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == title - assert result["data"] == expected_data - assert result["options"] == {} - assert len(mock_setup_entry.mock_calls) == 1 - - config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == expected_data - assert config_entry.options == {} - 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}" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data ) + # First step is confirmation, we haven't probed the firmware yet + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"]["firmware_type"] == "unknown" + assert result["description_placeholders"]["model"] == model -@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=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.serial_number = "bla_serial_number_2" - + # Next, we probe the firmware with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, # Ensure we re-install it ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" - -@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=title, - unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", + # Set up Zigbee firmware + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() ) - config_entry.add_to_hass(hass) - - usb_data = copy.copy(usb_data) - usb_data.device = "bla_device_2" - - with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - 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, + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), patch( - "homeassistant.components.homeassistant_sky_connect.async_unload_entry", - wraps=homeassistant_sky_connect.async_unload_entry, - ) as mock_unload_entry, + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, ) - await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_unload_entry.mock_calls) == 1 + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now configuring the addon and running it + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now uninstalling the addon + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_zigbee_flasher_addon" + assert result["progress_action"] == "uninstall_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # We are finally done with the addon + assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } + + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" @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, - install_addon, - set_addon_options, - start_addon, -) -> None: - """Test installing the multi pan addon.""" - assert await async_setup_component(hass, "usb", {}) - mock_integration(hass, MockModule("hassio")) - - # 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, - }, - domain=DOMAIN, - options={}, - 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) - - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "enable_multi_pan": True, - }, - ) - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "install_addon" - assert result["progress_action"] == "install_addon" - - await hass.async_block_till_done() - install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_addon" - set_addon_options.assert_called_once_with( - hass, - "core_silabs_multiprotocol", - { - "options": { - "autoflash_firmware": True, - "device": usb_data.device, - "baudrate": "115200", - "flow_control": True, - } - }, - ) - - await hass.async_block_till_done() - start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY - - -def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): - """Mock `detect_radio_type` that just sets the appropriate attributes.""" - - async def detect(self): - self.radio_type = radio_type - self.device_settings = radio_type.controller.SCHEMA_DEVICE( - {CONF_DEVICE_PATH: self.device_path} - ) - - return ret - - return detect - - -@pytest.mark.parametrize( - ("usb_data", "title"), + ("usb_data", "model"), [ (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, - install_addon, - set_addon_options, - start_addon, +async def test_config_flow_zigbee_skip_step_if_installed( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant ) -> None: - """Test installing the multi pan addon when a zha config entry exists.""" - assert await async_setup_component(hass, "usb", {}) - mock_integration(hass, MockModule("hassio")) + """Test the config flow for SkyConnect, skip installing the addon if necessary.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) - # 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, - }, - domain=DOMAIN, + # First step is confirmation, we haven't probed the firmware yet + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"]["firmware_type"] == "unknown" + assert result["description_placeholders"]["model"] == model + + # Next, we probe the firmware + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, # Ensure we re-install it + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" + + # Set up Zigbee firmware + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Pick the menu option: we skip installation, instead we directly run it + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + # Uninstall the addon + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + # Done + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the config flow for SkyConnect.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + # First step is confirmation, we haven't probed the firmware yet + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"]["firmware_type"] == "unknown" + assert result["description_placeholders"]["model"] == model + + # Next, we probe the firmware + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + + # Set up Thread firmware + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + # Pick the menu option + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Progress the flow, it is now configuring the addon and running it + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "spinel", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_already_installed( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the Thread config flow for SkyConnect, addon is already installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, options={}, - title=title, - unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", + state=AddonState.NOT_RUNNING, + update_available=False, + version=None, + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + # Pick the menu option + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_not_hassio( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test when the stick is used with a non-hassio setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } + + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_zigbee_to_thread( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow for SkyConnect, migrating Zigbee to Thread.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, ) config_entry.add_to_hass(hass) - zha_config_entry = MockConfigEntry( - data={"device": {"path": usb_data.device}, "radio_type": "ezsp"}, - domain=ZHA_DOMAIN, - options={}, - title="Yellow", - ) - zha_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + assert result["description_placeholders"]["model"] == model + + # Pick Thread + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Progress the flow, it is now configuring the addon and running it + result = await hass.config_entries.options.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # The firmware type has been updated + assert config_entry.data["firmware"] == "spinel" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_thread_to_zigbee( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow for SkyConnect, migrating Thread to Zigbee.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "spinel", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" + assert result["description_placeholders"]["model"] == model + + # Set up Zigbee firmware + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + # OTBR is not installed + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now configuring the addon and running it + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now uninstalling the addon + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_zigbee_flasher_addon" + assert result["progress_action"] == "uninstall_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # We are finally done with the addon + assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # The firmware type has been updated + assert config_entry.data["firmware"] == "ezsp" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_multipan_uninstall( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test options flow for when multi-PAN firmware is installed.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "cpc", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # Multi-PAN addon is running + mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass)) + mock_multipan_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": usb_data.device}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass)) + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + + with ( + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", + return_value=mock_multipan_manager, + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + return_value=True, + ), ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "addon_menu" + assert "uninstall_addon" in result["menu_options"] - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "enable_multi_pan": True, - }, - ) - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "install_addon" - assert result["progress_action"] == "install_addon" + # Pick the uninstall option + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": "uninstall_addon"}, + ) - await hass.async_block_till_done() - install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + # Check the box + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True} + ) - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_addon" - set_addon_options.assert_called_once_with( - hass, - "core_silabs_multiprotocol", - { - "options": { - "autoflash_firmware": True, - "device": usb_data.device, - "baudrate": "115200", - "flow_control": True, - } - }, - ) - # Check the ZHA config entry data is updated - assert zha_config_entry.data == { - "device": { - "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 115200, - "flow_control": None, - }, - "radio_type": "ezsp", - } + # Finish the flow + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + # We've reverted the firmware back to Zigbee + assert config_entry.data["firmware"] == "ezsp" diff --git a/tests/components/homeassistant_sky_connect/test_config_flow_failures.py b/tests/components/homeassistant_sky_connect/test_config_flow_failures.py new file mode 100644 index 00000000000..128c812272f --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_config_flow_failures.py @@ -0,0 +1,920 @@ +"""Test the Home Assistant SkyConnect config flow failure cases.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components import usb +from homeassistant.components.hassio.addon_manager import ( + AddonError, + AddonInfo, + AddonState, +) +from homeassistant.components.homeassistant_sky_connect.config_flow import ( + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, +) +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.components.homeassistant_sky_connect.util import ( + get_otbr_addon_manager, + get_zigbee_flasher_addon_manager, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .test_config_flow import USB_DATA_ZBT1, delayed_side_effect + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_cannot_probe_firmware( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when firmware cannot be probed.""" + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=None, + ): + # Start the flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + # Probing fails + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unsupported_firmware" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_not_hassio_wrong_firmware( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test when the stick is used with a non-hassio setup but the firmware is bad.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_hassio" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_already_running( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon is already running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + # Cannot get addon info + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_already_running" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_info_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.side_effect = AddonError() + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + # Cannot get addon info + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_install_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=AddonError() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + # Cannot install addon + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_set_config_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_set_addon_options = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_set_config_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_run_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon fails to run.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_uninstall_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon uninstall fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=AddonError() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + # Uninstall failure isn't critical + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_not_hassio( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test when the stick is used with a non-hassio setup and Thread is selected.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_hassio_thread" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_info_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.side_effect = AddonError() + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + # Cannot get addon info + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_already_running( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when the Thread addon is already running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + # Cannot install addon + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "otbr_addon_already_running" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_install_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + # Cannot install addon + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_set_config_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_set_addon_options = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_set_config_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_flasher_run_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon fails to run.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_flasher_uninstall_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon uninstall fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=AddonError() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + # Uninstall failure isn't critical + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_zigbee_to_thread_zha_configured( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow migration failure, ZHA using the stick.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # Set up ZHA as well + zha_config_entry = MockConfigEntry( + domain="zha", + data={"device": {"path": usb_data.device}}, + ) + zha_config_entry.add_to_hass(hass) + + # Confirm options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + # Pick Thread + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "zha_still_using_stick" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_thread_to_zigbee_otbr_configured( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow migration failure, OTBR still using the stick.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "spinel", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # Confirm options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + # Pick Zigbee + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": usb_data.device}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "otbr_still_using_stick" diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 6b283378045..888ed27a3c0 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -1,7 +1,5 @@ """Test the Home Assistant SkyConnect hardware platform.""" -from unittest.mock import patch - from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant from homeassistant.setup import async_setup_component @@ -15,7 +13,8 @@ CONFIG_ENTRY_DATA = { "pid": "EA60", "serial_number": "9e2adbd75b8beb119fe564a0f320645d", "manufacturer": "Nabu Casa", - "description": "SkyConnect v1.0", + "product": "SkyConnect v1.0", + "firmware": "ezsp", } CONFIG_ENTRY_DATA_2 = { @@ -24,7 +23,8 @@ CONFIG_ENTRY_DATA_2 = { "pid": "EA60", "serial_number": "9e2adbd75b8beb119fe564a0f320645d", "manufacturer": "Nabu Casa", - "description": "Home Assistant Connect ZBT-1", + "product": "Home Assistant Connect ZBT-1", + "firmware": "ezsp", } @@ -42,22 +42,24 @@ async def test_hardware_info( options={}, title="Home Assistant SkyConnect", unique_id="unique_1", + version=1, + minor_version=2, ) config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + config_entry_2 = MockConfigEntry( data=CONFIG_ENTRY_DATA_2, domain=DOMAIN, options={}, title="Home Assistant Connect ZBT-1", unique_id="unique_2", + version=1, + minor_version=2, ) config_entry_2.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(config_entry_2.entry_id) client = await hass_ws_client(hass) diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index a1fa4a5c743..88b57f2dd64 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,377 +1,56 @@ """Test the Home Assistant SkyConnect integration.""" -from collections.abc import Generator -from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import patch -import pytest +from universal_silabs_flasher.const import ApplicationType -from homeassistant.components import zha -from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.components.homeassistant_sky_connect.util import FirmwareGuess +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -CONFIG_ENTRY_DATA = { - "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", - "vid": "10C4", - "pid": "EA60", - "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", - "manufacturer": "Nabu Casa", - "description": "SkyConnect v1.0", -} +async def test_config_entry_migration_v2(hass: HomeAssistant) -> None: + """Test migrating config entries from v1 to v2 format.""" -@pytest.fixture(autouse=True) -def disable_usb_probing() -> Generator[None, None, None]: - """Disallow touching of system USB devices during unit tests.""" - with patch("homeassistant.components.usb.comports", return_value=[]): - yield - - -@pytest.fixture -def mock_zha_config_flow_setup() -> Generator[None, None, None]: - """Mock the radio connection and probing of the ZHA config flow.""" - - def mock_probe(config: dict[str, Any]) -> None: - # The radio probing will return the correct baudrate - return {**config, "baudrate": 115200} - - 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, - ), - ): - yield - - -@pytest.mark.parametrize( - ("onboarded", "num_entries", "num_flows"), [(False, 1, 0), (True, 0, 1)] -) -async def test_setup_entry( - mock_zha_config_flow_setup, - hass: HomeAssistant, - addon_store_info, - onboarded, - num_entries, - num_flows, -) -> None: - """Test setup of a config entry, including setup of zha.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - # Setup the config entry config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, domain=DOMAIN, - options={}, - 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, - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - - matcher = mock_is_plugged_in.mock_calls[0].args[1] - assert matcher["vid"].isupper() - assert matcher["pid"].isupper() - assert matcher["serial_number"].islower() - assert matcher["manufacturer"].islower() - assert matcher["description"].islower() - - # Finish setting up ZHA - if num_entries > 0: - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows - assert len(hass.config_entries.async_entries("zha")) == num_entries - - -async def test_setup_zha( - mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info -) -> None: - """Test zha gets the right config.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - 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 - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - # Finish setting up ZHA - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - config_entry = hass.config_entries.async_entries("zha")[0] - assert config_entry.data == { - "device": { - "baudrate": 115200, - "flow_control": None, - "path": CONFIG_ENTRY_DATA["device"], + unique_id="some_unique_id", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", }, - "radio_type": "ezsp", - } - assert config_entry.options == {} - assert config_entry.title == CONFIG_ENTRY_DATA["description"] - - -async def test_setup_zha_multipan( - hass: HomeAssistant, addon_info, addon_running -) -> None: - """Test zha gets the right config.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"] - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", + version=1, ) + 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), - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - # Finish setting up ZHA - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - config_entry = hass.config_entries.async_entries("zha")[0] - assert config_entry.data == { - "device": { - "baudrate": 115200, - "flow_control": None, - "path": "socket://core-silabs-multiprotocol:9999", - }, - "radio_type": "ezsp", - } - assert config_entry.options == {} - assert config_entry.title == "SkyConnect Multiprotocol" - - -async def test_setup_zha_multipan_other_device( - mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running -) -> None: - """Test zha gets the right config.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect" - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - 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), - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - - # Finish setting up ZHA - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - config_entry = hass.config_entries.async_entries("zha")[0] - assert config_entry.data == { - "device": { - "baudrate": 115200, - "flow_control": None, - "path": CONFIG_ENTRY_DATA["device"], - }, - "radio_type": "ezsp", - } - assert config_entry.options == {} - assert config_entry.title == CONFIG_ENTRY_DATA["description"] - - -async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: - """Test setup of a config entry when the dongle is not plugged in.""" - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - ) - config_entry.add_to_hass(hass) with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=False, - ) as mock_is_plugged_in: + "homeassistant.components.homeassistant_sky_connect.guess_firmware_type", + return_value=FirmwareGuess( + is_running=True, + firmware_type=ApplicationType.SPINEL, + source="otbr", + ), + ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - # USB discovery starts, config entry should be removed - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert config_entry.version == 1 + assert config_entry.minor_version == 2 + assert config_entry.data == { + "description": "SkyConnect v1.0", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", # `description` has been copied to `product` + "firmware": "spinel", # new key + } -async def test_setup_entry_addon_info_fails( - hass: HomeAssistant, addon_store_info -) -> None: - """Test setup of a config entry when fetching addon info fails.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - addon_store_info.side_effect = HassioAPIError("Boom") - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - 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), - ), - ): - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY - - -async def test_setup_entry_addon_not_running( - hass: HomeAssistant, addon_installed, start_addon -) -> None: - """Test the addon is started if it is not running.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - 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), - ), - ): - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY - start_addon.assert_called_once() + await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/homeassistant_sky_connect/test_util.py b/tests/components/homeassistant_sky_connect/test_util.py new file mode 100644 index 00000000000..12ba352eb16 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_util.py @@ -0,0 +1,203 @@ +"""Test SkyConnect utilities.""" + +from unittest.mock import AsyncMock, patch + +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components.hassio import AddonError, AddonInfo, AddonState +from homeassistant.components.homeassistant_sky_connect.const import ( + DOMAIN, + HardwareVariant, +) +from homeassistant.components.homeassistant_sky_connect.util import ( + FirmwareGuess, + get_hardware_variant, + get_usb_service_info, + get_zha_device_path, + guess_firmware_type, +) +from homeassistant.components.usb import UsbServiceInfo +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +SKYCONNECT_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", + "firmware": "ezsp", + }, + version=2, +) + +CONNECT_ZBT1_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "Home Assistant Connect ZBT-1", + "firmware": "ezsp", + }, + version=2, +) + +ZHA_CONFIG_ENTRY = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={ + "device": { + "path": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_3c0ed67c628beb11b1cd64a0f320645d-if00-port0", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, + version=4, +) + + +def test_get_usb_service_info() -> None: + """Test `get_usb_service_info` conversion.""" + assert get_usb_service_info(SKYCONNECT_CONFIG_ENTRY) == UsbServiceInfo( + device=SKYCONNECT_CONFIG_ENTRY.data["device"], + vid=SKYCONNECT_CONFIG_ENTRY.data["vid"], + pid=SKYCONNECT_CONFIG_ENTRY.data["pid"], + serial_number=SKYCONNECT_CONFIG_ENTRY.data["serial_number"], + manufacturer=SKYCONNECT_CONFIG_ENTRY.data["manufacturer"], + description=SKYCONNECT_CONFIG_ENTRY.data["product"], + ) + + +def test_get_hardware_variant() -> None: + """Test `get_hardware_variant` extraction.""" + assert get_hardware_variant(SKYCONNECT_CONFIG_ENTRY) == HardwareVariant.SKYCONNECT + assert ( + get_hardware_variant(CONNECT_ZBT1_CONFIG_ENTRY) == HardwareVariant.CONNECT_ZBT1 + ) + + +def test_get_zha_device_path() -> None: + """Test extracting the ZHA device path from its config entry.""" + assert ( + get_zha_device_path(ZHA_CONFIG_ENTRY) == ZHA_CONFIG_ENTRY.data["device"]["path"] + ) + + +async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None: + """Test guessing the firmware type.""" + + assert (await guess_firmware_type(hass, "/dev/missing")) == FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + ) + + +async def test_guess_firmware_type(hass: HomeAssistant) -> None: + """Test guessing the firmware.""" + path = ZHA_CONFIG_ENTRY.data["device"]["path"] + + ZHA_CONFIG_ENTRY.add_to_hass(hass) + + ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.NOT_LOADED) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="zha" + ) + + # When ZHA is running, we indicate as such when guessing + ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.LOADED) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager = AsyncMock() + mock_multipan_addon_manager = AsyncMock() + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.util.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.util.get_otbr_addon_manager", + return_value=mock_otbr_addon_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.util.get_multiprotocol_addon_manager", + return_value=mock_multipan_addon_manager, + ), + ): + mock_otbr_addon_manager.async_get_addon_info.side_effect = AddonError() + mock_multipan_addon_manager.async_get_addon_info.side_effect = AddonError() + + # Hassio errors are ignored and we still go with ZHA + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.side_effect = None + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": "/some/other/device"}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # We will prefer ZHA, as it is running (and actually pointing to the device) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + + # We will still prefer ZHA, as it is the one actually running + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # Finally, ZHA loses out to OTBR + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.SPINEL, source="otbr" + ) + + mock_multipan_addon_manager.async_get_addon_info.side_effect = None + mock_multipan_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # Which will lose out to multi-PAN + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol" + ) diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 821621d5e57..206ad4dce15 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -65,7 +65,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" assert result["data"] == {} assert result["options"] == {} @@ -99,7 +99,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() @@ -126,7 +126,7 @@ async def test_option_flow_install_multi_pan_addon( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", @@ -136,7 +136,7 @@ async def test_option_flow_install_multi_pan_addon( result["flow_id"], {"next_step_id": "multipan_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -145,7 +145,7 @@ async def test_option_flow_install_multi_pan_addon( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -153,7 +153,7 @@ async def test_option_flow_install_multi_pan_addon( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -172,7 +172,7 @@ async def test_option_flow_install_multi_pan_addon( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_zha( @@ -205,7 +205,7 @@ async def test_option_flow_install_multi_pan_addon_zha( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", @@ -215,7 +215,7 @@ async def test_option_flow_install_multi_pan_addon_zha( result["flow_id"], {"next_step_id": "multipan_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -224,7 +224,7 @@ async def test_option_flow_install_multi_pan_addon_zha( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -232,7 +232,7 @@ async def test_option_flow_install_multi_pan_addon_zha( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -260,7 +260,7 @@ async def test_option_flow_install_multi_pan_addon_zha( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -289,20 +289,20 @@ async def test_option_flow_led_settings( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], {"disk_led": False, "heartbeat_led": False, "power_led": False}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reboot_menu" set_yellow_settings.assert_called_once_with( hass, {"disk_led": False, "heartbeat_led": False, "power_led": False} @@ -312,7 +312,7 @@ async def test_option_flow_led_settings( result["flow_id"], {"next_step_id": reboot_menu_choice}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(reboot_host.mock_calls) == reboot_calls @@ -335,20 +335,20 @@ async def test_option_flow_led_settings_unchanged( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], {"disk_led": True, "heartbeat_led": True, "power_led": True}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY set_yellow_settings.assert_not_called() @@ -367,7 +367,7 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" with patch( @@ -378,7 +378,7 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "read_hw_settings_error" @@ -399,14 +399,14 @@ async def test_option_flow_led_settings_fail_2( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.homeassistant_yellow.config_flow.async_set_yellow_settings", @@ -416,5 +416,5 @@ async def test_option_flow_led_settings_fail_2( result["flow_id"], {"disk_led": False, "heartbeat_led": False, "power_led": False}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "write_hw_settings_error" diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index e94dbbc1438..ec3ba4e7005 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -44,7 +44,7 @@ async def test_setup_entry( ), ): 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) assert len(mock_get_os_info.mock_calls) == 1 @@ -90,7 +90,7 @@ async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None: ), ): 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) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -144,7 +144,7 @@ async def test_setup_zha_multipan( ), ): 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) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -198,7 +198,7 @@ async def test_setup_zha_multipan_other_device( ), ): 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) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -294,7 +294,7 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_addon_info_fails( @@ -325,7 +325,7 @@ async def test_setup_entry_addon_info_fails( assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_addon_not_running( @@ -355,5 +355,5 @@ async def test_setup_entry_addon_not_running( assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY start_addon.assert_called_once() diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index b3b8a70b1a1..ff47abab833 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.homekit.const import ( CONF_FILTER, DOMAIN, @@ -14,6 +14,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT, EntityCategory 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 homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component @@ -50,14 +51,14 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" with ( @@ -79,7 +80,7 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY bridge_name = (result3["title"].split(":"))[0] assert bridge_name == SHORT_BRIDGE_NAME assert result3["data"] == { @@ -112,14 +113,14 @@ async def test_setup_in_bridge_mode_name_taken( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" with ( @@ -141,7 +142,7 @@ async def test_setup_in_bridge_mode_name_taken( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] != SHORT_BRIDGE_NAME assert result3["title"].startswith(SHORT_BRIDGE_NAME) bridge_name = (result3["title"].split(":"))[0] @@ -198,14 +199,14 @@ async def test_setup_creates_entries_for_accessory_mode_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"include_domains": ["camera", "media_player", "light", "lock", "remote"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" with ( @@ -227,7 +228,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -272,7 +273,7 @@ async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "port_name_in_use" with ( @@ -291,7 +292,7 @@ async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "othername:56789" assert result2["data"] == { "name": "othername", @@ -316,7 +317,7 @@ async def test_options_flow_exclude_mode_advanced( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -327,14 +328,14 @@ async def test_options_flow_exclude_mode_advanced( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -343,7 +344,7 @@ async def test_options_flow_exclude_mode_advanced( user_input={}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [], "mode": "bridge", @@ -373,7 +374,7 @@ async def test_options_flow_exclude_mode_basic( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -384,7 +385,7 @@ async def test_options_flow_exclude_mode_basic( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] assert entities == ["climate.front_gate"] @@ -397,7 +398,7 @@ async def test_options_flow_exclude_mode_basic( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -458,7 +459,7 @@ async def test_options_flow_devices( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -469,7 +470,7 @@ async def test_options_flow_devices( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" entry = entity_registry.async_get("light.ceiling_lights") @@ -491,7 +492,7 @@ async def test_options_flow_devices( user_input={"devices": [device_id]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [device_id], "mode": "bridge", @@ -547,7 +548,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -558,7 +559,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -568,7 +569,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": ["1fabcabcabcabcabcabcabcabcabc"], "mode": "bridge", @@ -607,7 +608,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -618,7 +619,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" entities = result["data_schema"]({})["entities"] @@ -630,7 +631,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -668,7 +669,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -679,7 +680,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] @@ -691,7 +692,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -722,7 +723,7 @@ async def test_options_flow_include_mode_basic( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -733,14 +734,14 @@ async def test_options_flow_include_mode_basic( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.new"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -772,7 +773,7 @@ async def test_options_flow_exclude_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -783,7 +784,7 @@ async def test_options_flow_exclude_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -792,7 +793,7 @@ async def test_options_flow_exclude_mode_with_cameras( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -800,7 +801,7 @@ async def test_options_flow_exclude_mode_with_cameras( user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -818,7 +819,7 @@ async def test_options_flow_exclude_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -829,7 +830,7 @@ async def test_options_flow_exclude_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -838,7 +839,7 @@ async def test_options_flow_exclude_mode_with_cameras( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -846,7 +847,7 @@ async def test_options_flow_exclude_mode_with_cameras( user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", @@ -881,7 +882,7 @@ async def test_options_flow_include_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -892,7 +893,7 @@ async def test_options_flow_include_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -901,7 +902,7 @@ async def test_options_flow_include_mode_with_cameras( "entities": ["camera.native_h264", "camera.transcode_h264"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -909,7 +910,7 @@ async def test_options_flow_include_mode_with_cameras( user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -927,7 +928,7 @@ async def test_options_flow_include_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["climate", "fan", "vacuum", "camera"], @@ -952,7 +953,7 @@ async def test_options_flow_include_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.native_h264", "camera.transcode_h264"], @@ -969,7 +970,7 @@ async def test_options_flow_include_mode_with_cameras( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": ["camera.native_h264"], @@ -983,7 +984,7 @@ async def test_options_flow_include_mode_with_cameras( user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {}, "filter": { @@ -1017,7 +1018,7 @@ async def test_options_flow_with_camera_audio( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -1028,7 +1029,7 @@ async def test_options_flow_with_camera_audio( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -1037,7 +1038,7 @@ async def test_options_flow_with_camera_audio( "entities": ["camera.audio", "camera.no_audio"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -1045,7 +1046,7 @@ async def test_options_flow_with_camera_audio( user_input={"camera_audio": ["camera.audio"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1063,7 +1064,7 @@ async def test_options_flow_with_camera_audio( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["climate", "fan", "vacuum", "camera"], @@ -1088,7 +1089,7 @@ async def test_options_flow_with_camera_audio( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.audio", "camera.no_audio"], @@ -1105,7 +1106,7 @@ async def test_options_flow_with_camera_audio( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": [], @@ -1119,7 +1120,7 @@ async def test_options_flow_with_camera_audio( user_input={"camera_audio": []}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {}, "filter": { @@ -1164,7 +1165,7 @@ async def test_options_flow_blocked_when_from_yaml( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "yaml" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -1172,7 +1173,7 @@ async def test_options_flow_blocked_when_from_yaml( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -1197,7 +1198,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1217,7 +1218,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "accessory" assert _get_schema_default(result2["data_schema"].schema, "entities") is None @@ -1225,7 +1226,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1243,7 +1244,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["media_player"], @@ -1256,7 +1257,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "accessory" assert ( _get_schema_default(result2["data_schema"].schema, "entities") @@ -1267,7 +1268,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1289,14 +1290,14 @@ async def test_converting_bridge_to_accessory_mode( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" # We need to actually setup the config entry or the data @@ -1317,7 +1318,7 @@ async def test_converting_bridge_to_accessory_mode( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -1345,7 +1346,7 @@ async def test_converting_bridge_to_accessory_mode( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, "mode") == "bridge" @@ -1356,14 +1357,14 @@ async def test_converting_bridge_to_accessory_mode( user_input={"domains": ["camera"], "mode": "accessory"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "accessory" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": "camera.tv"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" with ( @@ -1379,7 +1380,7 @@ async def test_converting_bridge_to_accessory_mode( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.tv": {"video_codec": "copy"}}, "mode": "accessory", @@ -1444,7 +1445,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1468,7 +1469,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1490,7 +1491,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( ] }, ) - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1539,7 +1540,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1563,7 +1564,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1579,7 +1580,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( result2["flow_id"], user_input={"entities": ["media_player.tv", "switch.other"]}, ) - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1624,7 +1625,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1648,7 +1649,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "include" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1664,7 +1665,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( ] }, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index e8fb7e1d92e..7e924be1637 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -126,7 +126,7 @@ async def test_bridge_with_triggers( hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index c419f7c19e7..17e38a0a145 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -67,7 +67,6 @@ def _mock_socket(failure_attempts: int = 0) -> MagicMock: attempts += 1 if attempts <= failure_attempts: raise OSError - return mock_socket.bind = Mock(side_effect=_simulate_bind) return mock_socket diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 39466cc51e4..95bf2530b2d 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -195,8 +195,7 @@ async def setup_accessories_from_file(hass: HomeAssistant, path: str) -> Accesso load_fixture, os.path.join("homekit_controller", path) ) accessories_json = hkloads(accessories_fixture) - accessories = Accessories.from_list(accessories_json) - return accessories + return Accessories.from_list(accessories_json) async def setup_platform(hass): @@ -307,7 +306,7 @@ async def setup_test_component( config_entry, pairing = await setup_test_accessories(hass, [accessory], connection) entity = "testdevice" if suffix is None else f"testdevice_{suffix}" - return Helper(hass, ".".join((domain, entity)), pairing, accessory, config_entry) + return Helper(hass, f"{domain}.{entity}", pairing, accessory, config_entry) async def assert_devices_and_entities_created( diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 10f62920d8e..0507976cd20 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -26,7 +26,7 @@ 'model': 'AP2', 'name': 'Airversa AP2 1808', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1234', 'suggested_area': None, 'sw_version': '0.8.16', }), @@ -622,7 +622,7 @@ 'model': 'T8010', 'name': 'eufy HomeBase2-0AAA', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000A', 'suggested_area': None, 'sw_version': '2.1.6', }), @@ -695,7 +695,7 @@ 'model': 'T8113', 'name': 'eufyCam2-0000', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000D', 'suggested_area': None, 'sw_version': '1.6.7', }), @@ -936,7 +936,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000B', 'suggested_area': None, 'sw_version': '1.6.7', }), @@ -1177,7 +1177,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000C', 'suggested_area': None, 'sw_version': '1.6.7', }), @@ -1422,7 +1422,7 @@ 'model': 'HE1-G01', 'name': 'Aqara-Hub-E1-00A0', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00aa00000a0', 'suggested_area': None, 'sw_version': '3.3.0', }), @@ -1628,7 +1628,7 @@ 'model': 'AS006', 'name': 'Contact Sensor', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '158d0007c59c6a', 'suggested_area': None, 'sw_version': '0', }), @@ -1792,7 +1792,7 @@ 'model': 'ZHWA11LM', 'name': 'Aqara Hub-1563', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '0000000123456789', 'suggested_area': None, 'sw_version': '1.4.7', }), @@ -2067,7 +2067,7 @@ 'model': 'AR004', 'name': 'Programmable Switch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '111a1111a1a111', 'suggested_area': None, 'sw_version': '9', }), @@ -2190,7 +2190,7 @@ 'model': 'ABC1000', 'name': 'ArloBabyA0', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00A0000000000', 'suggested_area': None, 'sw_version': '1.10.931', }), @@ -2674,7 +2674,7 @@ 'model': 'CS-IWO', 'name': 'InWall Outlet-0394DE', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1020301376', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -3103,7 +3103,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -3262,7 +3262,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', }), @@ -3716,7 +3716,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -3875,7 +3875,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -4038,7 +4038,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', }), @@ -4496,7 +4496,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -4610,7 +4610,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', }), @@ -4891,7 +4891,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -5050,7 +5050,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -5213,7 +5213,7 @@ 'model': 'ECB501', 'name': 'My ecobee', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789016', 'suggested_area': None, 'sw_version': '4.7.340214', }), @@ -5680,7 +5680,7 @@ 'model': 'ecobee Switch+', 'name': 'Master Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '111111111111', 'suggested_area': None, 'sw_version': '4.5.130201', }), @@ -5969,7 +5969,7 @@ 'model': 'Eve Degree 00AAA0000', 'name': 'Eve Degree AA11', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.8', }), @@ -6325,7 +6325,7 @@ 'model': 'Eve Energy 20EAO8601', 'name': 'Eve Energy 50FF', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.9', }), @@ -6663,7 +6663,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', }), @@ -6864,7 +6864,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'C718B3-2', 'suggested_area': None, 'sw_version': '5.0.18', }), @@ -6981,7 +6981,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -7142,7 +7142,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -7215,7 +7215,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -7380,7 +7380,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -7500,7 +7500,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -7573,7 +7573,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -7698,7 +7698,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8020,7 +8020,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8097,7 +8097,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8170,7 +8170,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', }), @@ -8343,7 +8343,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -8504,7 +8504,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8577,7 +8577,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -8742,7 +8742,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -8862,7 +8862,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -8935,7 +8935,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -9061,7 +9061,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -9134,7 +9134,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -9260,7 +9260,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9591,7 +9591,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9668,7 +9668,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9741,7 +9741,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9921,7 +9921,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9994,7 +9994,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -10174,7 +10174,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -10247,7 +10247,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', }), @@ -10435,7 +10435,7 @@ 'model': 'Daikin-fwec3a-esp32-homekit-bridge', 'name': 'Air Conditioner', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00000001', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -10633,7 +10633,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462395276914', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -10769,7 +10769,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462395276939', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -10905,7 +10905,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462403113447', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11041,7 +11041,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462403233419', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11177,7 +11177,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462412411853', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11323,7 +11323,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462412413293', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11469,7 +11469,7 @@ 'model': 'RWL021', 'name': 'Hue dimmer switch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462389072572', 'suggested_area': None, 'sw_version': '45.1.17846', }), @@ -11784,7 +11784,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462378982941', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11907,7 +11907,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462378983942', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12030,7 +12030,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462379122122', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12153,7 +12153,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462379123707', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12276,7 +12276,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462383114163', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12399,7 +12399,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462383114193', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12522,7 +12522,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462385996792', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12645,7 +12645,7 @@ 'model': 'BSB002', 'name': 'Philips hue - 482544', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456', 'suggested_area': None, 'sw_version': '1.32.1932126170', }), @@ -12722,7 +12722,7 @@ 'model': 'LS1', 'name': 'Koogeek-LS1-20833F', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '2.2.15', }), @@ -12864,7 +12864,7 @@ 'model': 'P1EU', 'name': 'Koogeek-P1-A00AA0', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'EUCP03190xxxxx48', 'suggested_area': None, 'sw_version': '2.3.7', }), @@ -13027,7 +13027,7 @@ 'model': 'KH02CN', 'name': 'Koogeek-SW2-187A91', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'CNNT061751001372', 'suggested_area': None, 'sw_version': '1.0.3', }), @@ -13229,7 +13229,7 @@ 'model': 'E30 2B', 'name': 'Lennox', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'XXXXXXXX', 'suggested_area': None, 'sw_version': '3.40.XX', }), @@ -13509,7 +13509,7 @@ 'model': 'OLED55B9PUA', 'name': 'LG webOS TV AF80', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '999AAAAAA999', 'suggested_area': None, 'sw_version': '04.71.04', }), @@ -13688,7 +13688,7 @@ 'model': 'PD-FSQN-XX', 'name': 'Caséta® Wireless Fan Speed Control', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '39024290', 'suggested_area': None, 'sw_version': '001.005', }), @@ -13808,7 +13808,7 @@ 'model': 'L-BDG2-WH', 'name': 'Smart Bridge 2', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '12344331', 'suggested_area': None, 'sw_version': '08.08', }), @@ -13885,7 +13885,7 @@ 'model': 'MSS425F', 'name': 'MSS425F-15cc', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'HH41234', 'suggested_area': None, 'sw_version': '4.2.3', }), @@ -14162,7 +14162,7 @@ 'model': 'MSS565', 'name': 'MSS565-28da', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'BB1121', 'suggested_area': None, 'sw_version': '4.1.9', }), @@ -14289,7 +14289,7 @@ 'model': 'v1', 'name': 'Mysa-85dda9', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '2.8.1', }), @@ -14617,7 +14617,7 @@ 'model': 'NL55', 'name': 'Nanoleaf Strip 3B32', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '1.4.40', }), @@ -14887,7 +14887,7 @@ 'model': 'Netatmo Doorbell', 'name': 'Netatmo-Doorbell-g738658', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'g738658', 'suggested_area': None, 'sw_version': '80.0.0', }), @@ -15179,7 +15179,7 @@ 'model': 'Smart CO Alarm', 'name': 'Smart CO Alarm', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1234', 'suggested_area': None, 'sw_version': '1.0.3', }), @@ -15338,7 +15338,7 @@ 'model': 'Healthy Home Coach', 'name': 'Healthy Home Coach', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAAAAAAAAAAA', 'suggested_area': None, 'sw_version': '59', }), @@ -15639,7 +15639,7 @@ 'model': 'SPK5 Pro', 'name': 'RainMachine-00ce4a', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00aa0000aa0a', 'suggested_area': None, 'sw_version': '1.0.4', }), @@ -16060,7 +16060,7 @@ 'model': 'RYSE Shade', 'name': 'Master Bath South', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16221,7 +16221,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '0101.3521.0436', 'suggested_area': None, 'sw_version': '1.3.0', }), @@ -16294,7 +16294,7 @@ 'model': 'RYSE Shade', 'name': 'RYSE SmartShade', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '', 'suggested_area': None, 'sw_version': '', }), @@ -16459,7 +16459,7 @@ 'model': 'RYSE Shade', 'name': 'BR Left', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16620,7 +16620,7 @@ 'model': 'RYSE Shade', 'name': 'LR Left', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16781,7 +16781,7 @@ 'model': 'RYSE Shade', 'name': 'LR Right', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16942,7 +16942,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '0401.3521.0679', 'suggested_area': None, 'sw_version': '1.3.0', }), @@ -17015,7 +17015,7 @@ 'model': 'RYSE Shade', 'name': 'RZSS', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -17180,7 +17180,7 @@ 'model': 'BE479CAM619', 'name': 'SENSE ', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '004.027.000', }), @@ -17298,7 +17298,7 @@ 'model': 'SIMPLEconnect', 'name': 'SIMPLEconnect Fan-06F674', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1234567890abcd', 'suggested_area': None, 'sw_version': '', }), @@ -17473,7 +17473,7 @@ 'model': 'VELUX Gateway', 'name': 'VELUX Gateway', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'a1a11a1', 'suggested_area': None, 'sw_version': '70', }), @@ -17546,7 +17546,7 @@ 'model': 'VELUX Sensor', 'name': 'VELUX Sensor', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'a11b111', 'suggested_area': None, 'sw_version': '16', }), @@ -17754,7 +17754,7 @@ 'model': 'VELUX Window', 'name': 'VELUX Window', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1111111a114a111a', 'suggested_area': None, 'sw_version': '48', }), @@ -17874,7 +17874,7 @@ 'model': 'Flowerbud', 'name': 'VOCOlinc-Flowerbud-0d324b', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AM01121849000327', 'suggested_area': None, 'sw_version': '3.121.2', }), @@ -18178,7 +18178,7 @@ 'model': 'VP3', 'name': 'VOCOlinc-VP3-123456', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'EU0121203xxxxx07', 'suggested_area': None, 'sw_version': '1.101.2', }), diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 3f93ca1a896..059993e3bef 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -203,6 +203,7 @@ async def test_ecobee3_setup_connection_failure( # We just advance time by 5 minutes so that the retry happens, rather # than manually invoking async_setup_entry. await time_changed(hass, 5 * 60) + await hass.async_block_till_done(wait_background_tasks=True) climate = entity_registry.async_get("climate.homew") assert climate.unique_id == "00:00:00:00:00:00_1_16" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 6b658e9eef4..a336758f4ac 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -13,7 +13,7 @@ from aiohomekit.model.services import ServicesTypes from bleak.exc import BleakError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES @@ -243,7 +243,7 @@ async def test_discovery_works( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_ZEROCONF, @@ -253,14 +253,14 @@ async def test_discovery_works( # User initiates pairing - device enters pairing mode and displays code result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" # Pairing doesn't error error and pairing results result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} @@ -276,7 +276,7 @@ async def test_abort_duplicate_flow(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" result = await hass.config_entries.flow.async_init( @@ -284,7 +284,7 @@ async def test_abort_duplicate_flow(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -300,7 +300,7 @@ async def test_pair_already_paired_1(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_paired" @@ -317,7 +317,7 @@ async def test_unknown_domain_type(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -335,7 +335,7 @@ async def test_id_missing(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_properties" @@ -352,7 +352,7 @@ async def test_discovery_ignored_model(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -380,7 +380,7 @@ async def test_discovery_ignored_hk_bridge( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -408,7 +408,7 @@ async def test_discovery_does_not_ignore_non_homekit( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_discovery_broken_pairing_flag(hass: HomeAssistant, controller) -> None: @@ -483,7 +483,7 @@ async def test_discovery_invalid_config_entry(hass: HomeAssistant, controller) - assert config_entry_count == 0 # And new config flow should continue allowing user to set up a new pairing - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_discovery_ignored_config_entry(hass: HomeAssistant, controller) -> None: @@ -520,7 +520,7 @@ async def test_discovery_ignored_config_entry(hass: HomeAssistant, controller) - assert config_entry_count == 1 # We should abort since there is no accessory id in the data - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -549,7 +549,7 @@ async def test_discovery_already_configured(hass: HomeAssistant, controller) -> context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port @@ -587,7 +587,7 @@ async def test_discovery_already_configured_update_csharp( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -615,7 +615,7 @@ async def test_pair_abort_errors_on_start( test_exc = exception("error") with patch.object(device, "async_start_pairing", side_effect=test_exc): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == expected @@ -640,7 +640,7 @@ async def test_pair_try_later_errors_on_start( with patch.object(device, "async_start_pairing", side_effect=test_exc): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["step_id"] == expected - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM # Device is rebooted or placed into pairing mode as they have been instructed @@ -654,7 +654,7 @@ async def test_pair_try_later_errors_on_start( result3["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Koogeek-LS1-20833F" @@ -686,7 +686,7 @@ async def test_pair_form_errors_on_start( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { @@ -697,7 +697,7 @@ async def test_pair_form_errors_on_start( # User gets back the form result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # User re-tries entering pairing code @@ -705,7 +705,7 @@ async def test_pair_form_errors_on_start( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -736,7 +736,7 @@ async def test_pair_abort_errors_on_finish( with patch.object(device, "async_start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -747,7 +747,7 @@ async def test_pair_abort_errors_on_finish( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == expected @@ -778,7 +778,7 @@ async def test_pair_form_errors_on_finish( with patch.object(device, "async_start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -789,7 +789,7 @@ async def test_pair_form_errors_on_finish( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { @@ -826,7 +826,7 @@ async def test_pair_unknown_errors(hass: HomeAssistant, controller) -> None: with patch.object(device, "async_start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -837,7 +837,7 @@ async def test_pair_unknown_errors(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["pairing_code"] == "pairing_failed" assert ( result["description_placeholders"]["error"] == "The bluetooth connection failed" @@ -860,7 +860,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, @@ -869,7 +869,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"device": "TestDevice"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { @@ -881,7 +881,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -897,7 +897,7 @@ async def test_user_pairing_with_insecure_setup_code( "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, @@ -906,7 +906,7 @@ async def test_user_pairing_with_insecure_setup_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"device": "TestDevice"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { @@ -918,7 +918,7 @@ async def test_user_pairing_with_insecure_setup_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "123-45-678"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert result["errors"] == {"pairing_code": "insecure_setup_code"} @@ -926,7 +926,7 @@ async def test_user_pairing_with_insecure_setup_code( result["flow_id"], user_input={"pairing_code": "123-45-678", "allow_insecure_setup_codes": True}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -935,7 +935,7 @@ async def test_user_no_devices(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_init( "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices" @@ -952,7 +952,7 @@ async def test_user_no_unpaired_devices(hass: HomeAssistant, controller) -> None "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices" @@ -966,7 +966,7 @@ async def test_unignore_works(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": device.description.id}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Other"}, @@ -976,14 +976,14 @@ async def test_unignore_works(hass: HomeAssistant, controller) -> None: # User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" # Pairing finalized result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -1000,7 +1000,7 @@ async def test_unignore_ignores_missing_devices( data={"unique_id": "00:00:00:00:00:01"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "accessory_not_found_error" @@ -1022,7 +1022,7 @@ async def test_discovery_dismiss_existing_flow_on_paired( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" await hass.async_block_till_done() assert ( @@ -1038,7 +1038,7 @@ async def test_discovery_dismiss_existing_flow_on_paired( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_paired" await hass.async_block_till_done() assert ( @@ -1088,7 +1088,7 @@ async def test_mdns_update_to_paired_during_pairing( with patch.object(device, "async_start_pairing", _async_start_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -1110,11 +1110,11 @@ async def test_mdns_update_to_paired_during_pairing( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info_paired, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_paired" mdns_update_to_paired.set() result = await task - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} @@ -1130,7 +1130,7 @@ async def test_discovery_no_bluetooth_support(hass: HomeAssistant, controller) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1145,7 +1145,7 @@ async def test_bluetooth_not_homekit(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_HK_BLUETOOTH_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1162,7 +1162,7 @@ async def test_bluetooth_valid_device_no_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "accessory_not_found_error" @@ -1182,7 +1182,7 @@ async def test_bluetooth_valid_device_discovery_paired( data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_paired" @@ -1203,7 +1203,7 @@ async def test_bluetooth_valid_device_discovery_unpaired( data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert storage.get_map("00:00:00:00:00:00") is None @@ -1214,11 +1214,11 @@ async def test_bluetooth_valid_device_discovery_unpaired( } result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Koogeek-LS1-20833F" assert result3["data"] == {} @@ -1256,7 +1256,7 @@ async def test_discovery_updates_ip_when_config_entry_set_up( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -1294,7 +1294,7 @@ async def test_discovery_updates_ip_config_entry_not_set_up( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 40f565ec88b..b5a9aee72b1 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -5,7 +5,7 @@ from aiohomekit.model.services import ServicesTypes import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.homekit_controller.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -370,7 +370,7 @@ async def test_handle_events_late_setup( await hass.config_entries.async_unload(helper.config_entry.entry_id) await hass.async_block_till_done() - assert helper.config_entry.state == ConfigEntryState.NOT_LOADED + assert helper.config_entry.state is ConfigEntryState.NOT_LOADED assert await async_setup_component( hass, @@ -424,7 +424,7 @@ async def test_handle_events_late_setup( await hass.config_entries.async_setup(helper.config_entry.entry_id) await hass.async_block_till_done() - assert helper.config_entry.state == ConfigEntryState.LOADED + assert helper.config_entry.state is ConfigEntryState.LOADED # Make sure first automation (only) fires for single press helper.pairing.testing.update_named_service( diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index ec3e6216288..9d2022f6b1c 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -155,13 +155,13 @@ async def test_offline_device_raises(hass: HomeAssistant, controller) -> None: ) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY is_connected = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + await hass.async_block_till_done(wait_background_tasks=True) + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF @@ -212,21 +212,23 @@ async def test_ble_device_only_checks_is_available( ) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY is_available = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + await hass.async_block_till_done(wait_background_tasks=True) + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF is_available = False async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE is_available = True async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("light.testdevice").state == STATE_OFF diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 88298521f75..3f87f12d9fc 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -54,7 +54,7 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: HMIPC_NAME: "", HMIPC_PIN: HAPPIN, } - config_entry = MockConfigEntry( + return MockConfigEntry( version=1, domain=HMIPC_DOMAIN, title="Home Test SN", @@ -63,8 +63,6 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: source=SOURCE_IMPORT, ) - return config_entry - @pytest.fixture(name="default_mock_hap_factory") async def default_mock_hap_factory_fixture( diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index 83b5f8993bc..922601ca733 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -4791,6 +4791,59 @@ "type": "HEATING_THERMOSTAT", "updateState": "UP_TO_DATE" }, + "3014F71100000000ETRV0013": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000ETRV0013", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000014"], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -58, + "rssiPeerValue": -58, + "unreach": false, + "supportedOptionalFeatures": {} + }, + "1": { + "deviceId": "3014F71100000000ETRV0013", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": ["00000000-0000-0000-0005-000000000019"], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000ETRV0013", + "label": "Heizkörperthermostat4", + "lastStatusUpdate": 1524514007132, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000ETRV0013", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000014": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", @@ -8535,6 +8588,297 @@ "windowOpenTemperature": 5.0, "windowState": null }, + "00000000-0000-0000-0005-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F71100000000ETRV0013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0005-000000000019", + "label": "Vorzimmer3", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0000-0001-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0001-000000000019", + "label": "Vorzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0001-0001-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0001-0001-000000000019", + "label": "Vorzimmer2", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_1", + "name": "Testprofile", + "profileId": "00000000-0000-0000-0001-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000059", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, "00000000-AAAA-0000-0000-000000000001": { "actualTemperature": 15.4, "channels": [ diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 9ede89859dc..f175e2060df 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -1,6 +1,7 @@ """Tests for HomematicIP Cloud climate.""" import datetime +from unittest.mock import patch from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome @@ -15,7 +16,6 @@ from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, PRESET_ECO, - PRESET_NONE, HVACAction, HVACMode, ) @@ -217,12 +217,14 @@ async def test_hmip_heating_group_heat( ha_state = hass.states.get(entity_id) assert ha_state.state == HVACMode.AUTO + # hvac mode "dry" is not available. expect a valueerror. await hass.services.async_call( "climate", "set_hvac_mode", {"entity_id": entity_id, "hvac_mode": "dry"}, blocking=True, ) + assert len(hmip_device.mock_calls) == service_call_counter + 24 # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" @@ -429,14 +431,95 @@ async def test_hmip_heating_group_heat_with_radiator( assert ha_state.attributes["min_temp"] == 5.0 assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 - assert ha_state.attributes[ATTR_PRESET_MODE] is None + assert ha_state.attributes[ATTR_PRESET_MODE] == "Default" assert ha_state.attributes[ATTR_PRESET_MODES] == [ - PRESET_NONE, PRESET_BOOST, PRESET_ECO, + "Default", ] +async def test_hmip_heating_profile_default_name( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test visible profile 1 without a name should be displayed as 'Default'.""" + entity_id = "climate.vorzimmer3" + entity_name = "Vorzimmer3" + device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat4"], + test_groups=[entity_name], + ) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == HVACMode.AUTO + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "Default", + "Alternative 1", + ] + + +async def test_hmip_heating_profile_naming( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test Heating Profile Naming.""" + entity_id = "climate.vorzimmer2" + entity_name = "Vorzimmer2" + device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat2"], + test_groups=[entity_name], + ) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == HVACMode.AUTO + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "Testprofile", + "Alternative 1", + ] + + +async def test_hmip_heating_profile_name_not_in_list( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test set profile when profile is not in available profiles.""" + expected_profile = "Testprofile" + entity_id = "climate.vorzimmer2" + entity_name = "Vorzimmer2" + device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat2"], + test_groups=[entity_name], + ) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + with patch( + "homeassistant.components.homematicip_cloud.climate.NICE_PROFILE_NAMES", + return_value={}, + ): + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": expected_profile}, + blocking=True, + ) + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == expected_profile + + async def test_hmip_climate_services( hass: HomeAssistant, mock_hap_with_service ) -> None: diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 4b0d1c26b8f..d541bce4648 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_PIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -38,7 +39,7 @@ async def test_flow_works(hass: HomeAssistant, simple_mock_home) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "press_the_button"} @@ -70,7 +71,7 @@ async def test_flow_works(hass: HomeAssistant, simple_mock_home) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABC123" assert result["data"] == {"hapid": "ABC123", "authtoken": True, "name": "hmip"} assert result["result"].unique_id == "ABC123" @@ -88,7 +89,7 @@ async def test_flow_init_connection_error(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -114,7 +115,7 @@ async def test_flow_link_connection_error(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_aborted" @@ -136,7 +137,7 @@ async def test_flow_link_press_button(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "press_the_button"} @@ -147,7 +148,7 @@ async def test_init_flow_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -164,7 +165,7 @@ async def test_init_already_configured(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -193,7 +194,7 @@ async def test_import_config(hass: HomeAssistant, simple_mock_home) -> None: data=IMPORT_CONFIG, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABC123" assert result["data"] == {"authtoken": "123", "hapid": "ABC123", "name": "hmip"} assert result["result"].unique_id == "ABC123" @@ -222,5 +223,5 @@ async def test_import_existing_config(hass: HomeAssistant) -> None: data=IMPORT_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 9fc1f518c64..fb7fe7d7deb 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -26,7 +26,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 272 + assert len(mock_hap.hmip_device_by_entity_id) == 278 async def test_hmip_remove_device( diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index cddade7cec5..3cb8b7d61e9 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -105,7 +105,7 @@ async def test_hap_setup_connection_error() -> None: ): assert not await hap.async_setup() - assert not hass.async_add_hass_job.mock_calls + assert not hass.async_run_hass_job.mock_calls assert not hass.config_entries.flow.async_init.mock_calls diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index f0776877aec..8d12a8a1787 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -29,14 +29,14 @@ async def test_manual_flow_works( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -71,21 +71,21 @@ async def test_discovery_flow_works( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"ip_address": "127.0.0.1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot @@ -117,7 +117,7 @@ async def test_discovery_flow_during_onboarding( ), ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -154,7 +154,7 @@ async def test_discovery_flow_during_onboarding_disabled_api( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["errors"] == {"base": "api_not_enabled"} @@ -166,7 +166,7 @@ async def test_discovery_flow_during_onboarding_disabled_api( result["flow_id"], user_input={"ip_address": "127.0.0.1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -198,7 +198,7 @@ async def test_discovery_disabled_api( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" mock_homewizardenergy.device.side_effect = DisabledError @@ -207,7 +207,7 @@ async def test_discovery_disabled_api( result["flow_id"], user_input={"ip_address": "127.0.0.1"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "api_not_enabled"} @@ -233,7 +233,7 @@ async def test_discovery_missing_data_in_service_info(hass: HomeAssistant) -> No ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_parameters" @@ -259,7 +259,7 @@ async def test_discovery_invalid_api(hass: HomeAssistant) -> None: ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -281,14 +281,14 @@ async def test_error_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["data_schema"]({}) == {CONF_IP_ADDRESS: "127.0.0.1"} @@ -299,7 +299,7 @@ async def test_error_flow( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -322,14 +322,14 @@ async def test_abort_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -349,12 +349,12 @@ async def test_reauth_flow( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -375,10 +375,10 @@ async def test_reauth_error( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "api_not_enabled"} diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index a66e743fcd6..d00b5a13150 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -47,7 +47,7 @@ async def test_user_flow( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Main controller" assert result["data"] == {} assert result["options"] == { @@ -81,7 +81,7 @@ async def test_user_flow_already_exists( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "duplicated_host_port"} @@ -93,7 +93,7 @@ async def test_user_flow_already_exists( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "duplicated_controller_id"} @@ -124,7 +124,7 @@ async def test_user_flow_cannot_connect( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} assert result["step_id"] == "user" @@ -164,19 +164,19 @@ async def test_import_flow( ], }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Main controller" assert result["data"] == {} assert result["options"] == { @@ -211,7 +211,7 @@ async def test_import_flow_already_exists( context={"source": SOURCE_IMPORT}, data={"host": "192.168.0.1", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(issue_registry.issues) == 1 @@ -226,13 +226,13 @@ async def test_import_flow_controller_id_exists( context={"source": SOURCE_IMPORT}, data={"host": "192.168.0.2", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "import_controller_name" assert result["errors"] == {"base": "duplicated_controller_id"} @@ -247,7 +247,7 @@ async def test_reconfigure_flow( DOMAIN, context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( @@ -258,7 +258,7 @@ async def test_reconfigure_flow( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.options == { "controller_id": "main_controller", @@ -315,7 +315,7 @@ async def test_reconfigure_flow_flow_duplicate( DOMAIN, context={"source": SOURCE_RECONFIGURE, "entry_id": entry1.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( @@ -325,7 +325,7 @@ async def test_reconfigure_flow_flow_duplicate( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" assert result["errors"] == {"base": "duplicated_host_port"} @@ -340,7 +340,7 @@ async def test_reconfigure_flow_flow_no_change( DOMAIN, context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( @@ -350,7 +350,7 @@ async def test_reconfigure_flow_flow_no_change( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.options == { "controller_id": "main_controller", @@ -392,14 +392,14 @@ async def test_options_add_light_flow( result = await hass.config_entries.options.async_init( mock_empty_config_entry.entry_id ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "add_light" result = await hass.config_entries.options.async_configure( @@ -410,7 +410,7 @@ async def test_options_add_light_flow( CONF_RATE: 2.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -439,14 +439,14 @@ async def test_options_add_remove_light_flow( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "add_light" result = await hass.config_entries.options.async_configure( @@ -457,7 +457,7 @@ async def test_options_add_remove_light_flow( CONF_RATE: 2.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -493,14 +493,14 @@ async def test_options_add_remove_light_flow( # 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "remove_light" assert result["data_schema"].schema["index"].options == { "0": "Foyer Sconces ([02:08:01:01])", @@ -510,7 +510,7 @@ async def test_options_add_remove_light_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_INDEX: ["0"]} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -557,14 +557,14 @@ async def test_options_add_remove_keypad_flow( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" result = await hass.config_entries.options.async_configure( @@ -574,7 +574,7 @@ async def test_options_add_remove_keypad_flow( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -605,14 +605,14 @@ async def test_options_add_remove_keypad_flow( # 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "remove_keypad" assert result["data_schema"].schema["index"].options == { "0": "Foyer Keypad ([02:08:02:01])", @@ -622,7 +622,7 @@ async def test_options_add_remove_keypad_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_INDEX: ["0"]} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -644,14 +644,14 @@ async def test_options_add_keypad_with_error( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" # Try an invalid address @@ -662,7 +662,7 @@ async def test_options_add_keypad_with_error( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" assert result["errors"] == {"base": "invalid_addr"} @@ -674,7 +674,7 @@ async def test_options_add_keypad_with_error( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" assert result["errors"] == {"base": "duplicated_addr"} @@ -686,7 +686,7 @@ async def test_options_add_keypad_with_error( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" assert result["errors"] == {"base": "duplicated_addr"} @@ -701,13 +701,13 @@ async def test_options_edit_light_no_lights_flow( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_light" assert result["data_schema"].schema["index"].container == { "0": "Foyer Sconces ([02:08:01:01])" @@ -717,7 +717,7 @@ async def test_options_edit_light_no_lights_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_light" result = await hass.config_entries.options.async_configure( @@ -725,7 +725,7 @@ async def test_options_edit_light_no_lights_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 3.0}], @@ -769,13 +769,13 @@ async def test_options_edit_light_flow_empty( result = await hass.config_entries.options.async_init( mock_empty_config_entry.entry_id ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_light" assert result["data_schema"].schema["index"].container == {} @@ -791,13 +791,13 @@ async def test_options_add_button_flow( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -807,14 +807,14 @@ async def test_options_add_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "add_button" result = await hass.config_entries.options.async_configure( @@ -828,7 +828,7 @@ async def test_options_add_button_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], @@ -876,13 +876,13 @@ async def test_options_add_button_flow_duplicate( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -892,14 +892,14 @@ async def test_options_add_button_flow_duplicate( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "add_button" result = await hass.config_entries.options.async_configure( @@ -911,7 +911,7 @@ async def test_options_add_button_flow_duplicate( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "duplicated_number"} @@ -926,13 +926,13 @@ async def test_options_edit_button_flow( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -942,13 +942,13 @@ async def test_options_edit_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_button" assert result["data_schema"].schema["index"].container == { "0": "Morning (1)", @@ -960,7 +960,7 @@ async def test_options_edit_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_button" result = await hass.config_entries.options.async_configure( @@ -972,7 +972,7 @@ async def test_options_edit_button_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], @@ -1013,13 +1013,13 @@ async def test_options_remove_button_flow( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -1029,14 +1029,14 @@ async def test_options_remove_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "remove_button" assert result["data_schema"].schema["index"].options == { "0": "Morning (1)", @@ -1048,7 +1048,7 @@ async def test_options_remove_button_flow( result["flow_id"], user_input={CONF_INDEX: ["0"]} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py index 566e0b4beb4..1969bb448ec 100644 --- a/tests/components/homeworks/test_init.py +++ b/tests/components/homeworks/test_init.py @@ -3,12 +3,14 @@ from unittest.mock import ANY, MagicMock from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED +import pytest 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.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_capture_events @@ -114,3 +116,77 @@ async def test_keypad_events( await hass.async_block_till_done() assert len(press_events) == 1 assert len(release_events) == 1 + + +async def test_send_command( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test the send command service.""" + 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_controller._send.reset_mock() + await hass.services.async_call( + DOMAIN, + "send_command", + {"controller_id": "main_controller", "command": "KBP, [02:08:02:01], 1"}, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 1 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + + mock_controller._send.reset_mock() + await hass.services.async_call( + DOMAIN, + "send_command", + { + "controller_id": "main_controller", + "command": [ + "KBP, [02:08:02:01], 1", + "KBH, [02:08:02:01], 1", + "KBR, [02:08:02:01], 1", + ], + }, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 3 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[1][1] == ("KBH, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[2][1] == ("KBR, [02:08:02:01], 1",) + + mock_controller._send.reset_mock() + await hass.services.async_call( + DOMAIN, + "send_command", + { + "controller_id": "main_controller", + "command": [ + "KBP, [02:08:02:01], 1", + "delay 50", + "KBH, [02:08:02:01], 1", + "dElAy 100", + "KBR, [02:08:02:01], 1", + ], + }, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 3 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[1][1] == ("KBH, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[2][1] == ("KBR, [02:08:02:01], 1",) + + mock_controller._send.reset_mock() + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + "send_command", + {"controller_id": "unknown_controller", "command": "KBP, [02:08:02:01], 1"}, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 0 diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 751ba8aa288..d09444808d8 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -38,7 +38,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -205,6 +205,16 @@ async def test_mode_service_calls( ) device.set_system_mode.assert_called_once_with("auto") + device.set_system_mode.reset_mock() + device.set_system_mode.side_effect = aiosomecomfort.UnexpectedResponse + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + blocking=True, + ) + async def test_auxheat_service_calls( hass: HomeAssistant, device: MagicMock, config_entry: MagicMock @@ -300,6 +310,15 @@ async def test_fan_modes_service_calls( blocking=True, ) + device.set_fan_mode.side_effect = aiosomecomfort.UnexpectedResponse + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_DIFFUSE}, + blocking=True, + ) + async def test_service_calls_off_mode( hass: HomeAssistant, @@ -344,7 +363,7 @@ async def test_service_calls_off_mode( device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError caplog.clear() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -431,6 +450,12 @@ async def test_service_calls_off_mode( device.set_hold_heat.assert_called_once_with(False) device.set_hold_cool.assert_called_once_with(False) + device.set_hold_heat.reset_mock() + device.set_hold_cool.reset_mock() + + device.set_setpoint_cool.reset_mock() + device.set_setpoint_heat.reset_mock() + reset_mock(device) device.raw_ui_data["StatusHeat"] = 2 @@ -506,7 +531,7 @@ async def test_service_calls_cool_mode( device.set_setpoint_cool.reset_mock() device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -538,7 +563,7 @@ async def test_service_calls_cool_mode( device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError caplog.clear() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -570,7 +595,7 @@ async def test_service_calls_cool_mode( device.hold_heat = True device.hold_cool = True - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -709,7 +734,7 @@ async def test_service_calls_heat_mode( device.set_hold_heat.reset_mock() device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -747,7 +772,7 @@ async def test_service_calls_heat_mode( device.set_setpoint_heat.reset_mock() device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -780,7 +805,7 @@ async def test_service_calls_heat_mode( device.hold_heat = True device.hold_cool = True - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -811,7 +836,7 @@ async def test_service_calls_heat_mode( reset_mock(device) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -828,7 +853,7 @@ async def test_service_calls_heat_mode( device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -841,6 +866,16 @@ async def test_service_calls_heat_mode( device.set_setpoint_cool.assert_not_called() assert "Temperature out of range" in caplog.text + device.set_hold_heat.side_effect = aiosomecomfort.UnexpectedResponse + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + reset_mock(device) caplog.clear() with pytest.raises(HomeAssistantError): @@ -951,7 +986,7 @@ async def test_service_calls_auto_mode( device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -966,7 +1001,7 @@ async def test_service_calls_auto_mode( device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -1021,7 +1056,7 @@ async def test_service_calls_auto_mode( device.set_setpoint_heat.side_effect = None device.set_setpoint_cool.side_effect = None - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index a978a14daa1..7cd987f0d83 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch import aiosomecomfort import pytest -from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, @@ -33,7 +32,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -67,7 +66,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG @@ -87,7 +86,7 @@ async def test_show_option_form( ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -114,7 +113,7 @@ async def test_create_option_entry( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, @@ -147,7 +146,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -160,7 +159,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "new-username", @@ -190,7 +189,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} client.login.side_effect = aiosomecomfort.device.AuthError @@ -204,7 +203,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -239,7 +238,7 @@ async def test_reauth_flow_connnection_error( await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} client.login.side_effect = error @@ -250,5 +249,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/honeywell/test_diagnostics.py b/tests/components/honeywell/test_diagnostics.py index b180bf0e5bc..06c41d3d055 100644 --- a/tests/components/honeywell/test_diagnostics.py +++ b/tests/components/honeywell/test_diagnostics.py @@ -29,7 +29,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 6 + assert hass.states.async_entity_ids_count() == 8 result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index d27428fcf65..a77c0aaed7e 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -30,7 +30,7 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 + hass.states.async_entity_ids_count() == 4 ) # 1 climate entity; 2 sensor entities @@ -63,8 +63,8 @@ async def test_setup_multiple_thermostats( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 6 - ) # 2 climate entities; 4 sensor entities + hass.states.async_entity_ids_count() == 8 + ) # 2 climate entities; 4 sensor entities; 2 switch entities async def test_setup_multiple_thermostats_with_same_deviceid( @@ -84,8 +84,8 @@ async def test_setup_multiple_thermostats_with_same_deviceid( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 - ) # 1 climate entity; 2 sensor entities + hass.states.async_entity_ids_count() == 4 + ) # 1 climate entity; 2 sensor entities; 1 switch enitiy assert "Platform honeywell does not generate unique IDs" not in caplog.text @@ -171,7 +171,7 @@ async def test_remove_stale_device( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 6 + assert hass.states.async_entity_ids_count() == 8 device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id @@ -209,8 +209,8 @@ async def test_remove_stale_device( assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 - ) # 1 climate entities; 2 sensor entities + hass.states.async_entity_ids_count() == 4 + ) # 1 climate entities; 2 sensor entities; 1 switch entity device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py new file mode 100644 index 00000000000..73052871ef1 --- /dev/null +++ b/tests/components/honeywell/test_switch.py @@ -0,0 +1,87 @@ +"""Tests for Honeywell switch component.""" + +from unittest.mock import MagicMock + +from aiosomecomfort.exceptions import SomeComfortError +import pytest + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import init_integration + +from tests.common import MockConfigEntry + + +async def test_emheat_switch( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device: MagicMock, +) -> None: + """Test emergency heat switch.""" + + await init_integration(hass, config_entry) + entity_id = f"switch.{device.name}_emergency_heat" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_not_called() + + device.set_system_mode.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_not_called() + + device.system_mode = "heat" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("emheat") + + device.set_system_mode.reset_mock() + device.system_mode = "emheat" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") + + device.set_system_mode.reset_mock() + device.system_mode = "heat" + device.set_system_mode.side_effect = SomeComfortError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("emheat") + + device.set_system_mode.reset_mock() + device.system_mode = "emheat" + device.set_system_mode.side_effect = SomeComfortError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 6763708cc38..ec14b38cd69 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -2,7 +2,7 @@ from http import HTTPStatus import json -from unittest.mock import MagicMock, mock_open, patch +from unittest.mock import mock_open, patch from aiohttp.hdrs import AUTHORIZATION @@ -83,166 +83,174 @@ async def mock_client(hass, hass_client, registrations=None): return await hass_client() -class TestHtml5Notify: - """Tests for HTML5 notify platform.""" +async def test_get_service_with_no_json(hass: HomeAssistant): + """Test empty json file.""" + await async_setup_component(hass, "http", {}) + m = mock_open() + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) - def test_get_service_with_no_json(self): - """Test empty json file.""" - hass = MagicMock() + assert service is not None - m = mock_open() - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) - assert service is not None +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_dismissing_message(mock_wp, hass: HomeAssistant): + """Test dismissing message.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - @patch("homeassistant.components.html5.notify.WebPusher") - def test_dismissing_message(self, mock_wp): - """Test dismissing message.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + data = {"device": SUBSCRIPTION_1} - data = {"device": SUBSCRIPTION_1} + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + assert service is not None - assert service is not None + await service.async_dismiss(target=["device", "non_existing"], data={"tag": "test"}) - service.dismiss(target=["device", "non_existing"], data={"tag": "test"}) + assert len(mock_wp.mock_calls) == 4 - assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] + # Call to send + payload = json.loads(mock_wp.mock_calls[3][2]["data"]) - # Call to send - payload = json.loads(mock_wp.mock_calls[3][2]["data"]) + assert payload["dismiss"] is True + assert payload["tag"] == "test" - assert payload["dismiss"] is True - assert payload["tag"] == "test" - @patch("homeassistant.components.html5.notify.WebPusher") - def test_sending_message(self, mock_wp): - """Test sending message.""" - hass = MagicMock() - mock_wp().send().status_code = 201 +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_sending_message(mock_wp, hass: HomeAssistant): + """Test sending message.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - data = {"device": SUBSCRIPTION_1} + data = {"device": SUBSCRIPTION_1} - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - assert service is not None + assert service is not None - service.send_message( - "Hello", target=["device", "non_existing"], data={"icon": "beer.png"} - ) + await service.async_send_message( + "Hello", target=["device", "non_existing"], data={"icon": "beer.png"} + ) - assert len(mock_wp.mock_calls) == 4 + assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] - # Call to send - payload = json.loads(mock_wp.mock_calls[3][2]["data"]) + # Call to send + payload = json.loads(mock_wp.mock_calls[3][2]["data"]) - assert payload["body"] == "Hello" - assert payload["icon"] == "beer.png" + assert payload["body"] == "Hello" + assert payload["icon"] == "beer.png" - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_key_include(self, mock_wp): - """Test if the FCM header is included.""" - hass = MagicMock() - mock_wp().send().status_code = 201 - data = {"chrome": SUBSCRIPTION_5} +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_key_include(mock_wp, hass: HomeAssistant): + """Test if the FCM header is included.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + data = {"chrome": SUBSCRIPTION_5} - assert service is not None + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - service.send_message("Hello", target=["chrome"]) + assert service is not None - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + await service.async_send_message("Hello", target=["chrome"]) - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["Authorization"] is not None + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_send_with_unknown_priority(self, mock_wp): - """Test if the gcm_key is only included for GCM endpoints.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["Authorization"] is not None - data = {"chrome": SUBSCRIPTION_5} - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant): + """Test if the gcm_key is only included for GCM endpoints.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - assert service is not None + data = {"chrome": SUBSCRIPTION_5} - service.send_message("Hello", target=["chrome"], priority="undefined") + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + assert service is not None - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" + await service.async_send_message("Hello", target=["chrome"], priority="undefined") - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_no_targets(self, mock_wp): - """Test if the gcm_key is only included for GCM endpoints.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] - data = {"chrome": SUBSCRIPTION_5} + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) - assert service is not None +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_no_targets(mock_wp, hass: HomeAssistant): + """Test if the gcm_key is only included for GCM endpoints.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - service.send_message("Hello") + data = {"chrome": SUBSCRIPTION_5} - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" + assert service is not None - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_additional_data(self, mock_wp): - """Test if the gcm_key is only included for GCM endpoints.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + await service.async_send_message("Hello") - data = {"chrome": SUBSCRIPTION_5} + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" - assert service is not None - service.send_message("Hello", data={"mykey": "myvalue"}) +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_additional_data(mock_wp, hass: HomeAssistant): + """Test if the gcm_key is only included for GCM endpoints.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + data = {"chrome": SUBSCRIPTION_5} - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass + + assert service is not None + + await service.async_send_message("Hello", data={"mykey": "myvalue"}) + + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" async def test_registering_new_device_view( diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index de6f323bc8a..afff8294f0c 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,22 +1,28 @@ """The tests for the Home Assistant HTTP component.""" +from collections.abc import Awaitable, Callable from datetime import timedelta from http import HTTPStatus from ipaddress import ip_network +import logging from unittest.mock import Mock, patch -from aiohttp import BasicAuth, web +from aiohttp import BasicAuth, ServerDisconnectedError, web +from aiohttp.test_utils import TestClient from aiohttp.web_exceptions import HTTPUnauthorized +from aiohttp_session import get_session import jwt import pytest import yarl +from yarl import URL from homeassistant.auth.const import GROUP_ID_READ_ONLY -from homeassistant.auth.models import User +from homeassistant.auth.models import RefreshToken, User from homeassistant.auth.providers import trusted_networks from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) +from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT from homeassistant.components import websocket_api from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import ( @@ -24,11 +30,12 @@ from homeassistant.components.http.auth import ( DATA_SIGN_SECRET, SIGN_QUERY_PARAM, STORAGE_KEY, + STRICT_CONNECTION_GUARD_PAGE, async_setup_auth, async_sign_path, async_user_not_allowed_do_auth, ) -from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode from homeassistant.components.http.forwarded import async_setup_forwarded from homeassistant.components.http.request_context import ( current_request, @@ -36,13 +43,15 @@ from homeassistant.components.http.request_context import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from . import HTTP_HEADER_HA_AUTH -from tests.common import MockUser +from tests.common import MockUser, async_fire_time_changed from tests.test_util import mock_real_ip from tests.typing import ClientSessionGenerator, WebSocketGenerator +_LOGGER = logging.getLogger(__name__) API_PASSWORD = "test-password" # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases @@ -54,7 +63,13 @@ TRUSTED_NETWORKS = [ ] TRUSTED_ADDRESSES = ["100.64.0.1", "192.0.2.100", "FD01:DB8::1", "2001:DB8:ABCD::1"] EXTERNAL_ADDRESSES = ["198.51.100.1", "2001:DB8:FA1::1"] -UNTRUSTED_ADDRESSES = [*EXTERNAL_ADDRESSES, "127.0.0.1", "::1"] +LOCALHOST_ADDRESSES = ["127.0.0.1", "::1"] +UNTRUSTED_ADDRESSES = [*EXTERNAL_ADDRESSES, *LOCALHOST_ADDRESSES] +PRIVATE_ADDRESSES = [ + "192.168.10.10", + "172.16.4.20", + "10.100.50.5", +] async def mock_handler(request): @@ -122,7 +137,7 @@ async def test_cant_access_with_password_in_header( hass: HomeAssistant, ) -> None: """Test access with password in header.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -139,7 +154,7 @@ async def test_cant_access_with_password_in_query( hass: HomeAssistant, ) -> None: """Test access with password in URL.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) resp = await client.get("/", params={"api_password": API_PASSWORD}) @@ -159,7 +174,7 @@ async def test_basic_auth_does_not_work( legacy_auth: LegacyApiPasswordAuthProvider, ) -> None: """Test access with basic authentication.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) @@ -183,7 +198,7 @@ async def test_cannot_access_with_trusted_ip( hass_owner_user: MockUser, ) -> None: """Test access with an untrusted ip address.""" - await async_setup_auth(hass, app2) + await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED) set_mock_ip = mock_real_ip(app2) client = await aiohttp_client(app2) @@ -211,7 +226,7 @@ async def test_auth_active_access_with_access_token_in_header( ) -> None: """Test access with access token in header.""" token = hass_access_token - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -247,7 +262,7 @@ async def test_auth_active_access_with_trusted_ip( hass_owner_user: MockUser, ) -> None: """Test access with an untrusted ip address.""" - await async_setup_auth(hass, app2) + await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED) set_mock_ip = mock_real_ip(app2) client = await aiohttp_client(app2) @@ -274,7 +289,7 @@ async def test_auth_legacy_support_api_password_cannot_access( hass: HomeAssistant, ) -> None: """Test access using api_password if auth.support_legacy.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -296,7 +311,7 @@ async def test_auth_access_signed_path_with_refresh_token( """Test access with signed url.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -341,7 +356,7 @@ async def test_auth_access_signed_path_with_query_param( """Test access with signed url and query params.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -371,7 +386,7 @@ async def test_auth_access_signed_path_with_query_param_order( """Test access with signed url and query params different order.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -412,7 +427,7 @@ async def test_auth_access_signed_path_with_query_param_safe_param( """Test access with signed url and changing a safe param.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -451,7 +466,7 @@ async def test_auth_access_signed_path_with_query_param_tamper( """Test access with signed url and query params that have been tampered with.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -520,7 +535,7 @@ async def test_auth_access_signed_path_with_http( ) app.router.add_get("/hello", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -544,7 +559,7 @@ async def test_auth_access_signed_path_with_content_user( hass: HomeAssistant, app, aiohttp_client: ClientSessionGenerator ) -> None: """Test access signed url uses content user.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) signed_path = async_sign_path(hass, "/", timedelta(seconds=5)) signature = yarl.URL(signed_path).query["authSig"] claims = jwt.decode( @@ -564,7 +579,7 @@ async def test_local_only_user_rejected( ) -> None: """Test access with access token in header.""" token = hass_access_token - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) set_mock_ip = mock_real_ip(app) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -630,7 +645,7 @@ async def test_create_user_once(hass: HomeAssistant) -> None: """Test that we reuse the user.""" cur_users = len(await hass.auth.async_get_users()) app = web.Application() - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) users = await hass.auth.async_get_users() assert len(users) == cur_users + 1 @@ -642,7 +657,287 @@ async def test_create_user_once(hass: HomeAssistant) -> None: assert len(user.refresh_tokens) == 1 assert user.system_generated - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) # test it did not create a user assert len(await hass.auth.async_get_users()) == cur_users + 1 + + +@pytest.fixture +def app_strict_connection(hass): + """Fixture to set up a web.Application.""" + + async def handler(request): + """Return if request was authenticated.""" + return web.json_response(data={"authenticated": request[KEY_AUTHENTICATED]}) + + app = web.Application() + app[KEY_HASS] = hass + app.router.add_get("/", handler) + async_setup_forwarded(app, True, []) + return app + + +@pytest.mark.parametrize( + "strict_connection_mode", [e.value for e in StrictConnectionMode] +) +async def test_strict_connection_non_cloud_authenticated_requests( + hass: HomeAssistant, + app_strict_connection: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test authenticated requests with strict connection.""" + token = hass_access_token + await async_setup_auth(hass, app_strict_connection, strict_connection_mode) + set_mock_ip = mock_real_ip(app_strict_connection) + client = await aiohttp_client(app_strict_connection) + refresh_token = hass.auth.async_validate_access_token(hass_access_token) + assert refresh_token + assert hass.auth.session._strict_connection_sessions == {} + + signed_path = async_sign_path( + hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + + for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES, *EXTERNAL_ADDRESSES): + set_mock_ip(remote_addr) + + # authorized requests should work normally + req = await client.get("/", headers={"Authorization": f"Bearer {token}"}) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + + +@pytest.mark.parametrize( + "strict_connection_mode", [e.value for e in StrictConnectionMode] +) +async def test_strict_connection_non_cloud_local_unauthenticated_requests( + hass: HomeAssistant, + app_strict_connection: web.Application, + aiohttp_client: ClientSessionGenerator, + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test local unauthenticated requests with strict connection.""" + await async_setup_auth(hass, app_strict_connection, strict_connection_mode) + set_mock_ip = mock_real_ip(app_strict_connection) + client = await aiohttp_client(app_strict_connection) + assert hass.auth.session._strict_connection_sessions == {} + + for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES): + set_mock_ip(remote_addr) + # local requests should work normally + req = await client.get("/") + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": False} + + +def _add_set_cookie_endpoint(app: web.Application, refresh_token: RefreshToken) -> None: + """Add an endpoint to set a cookie.""" + + async def set_cookie(request: web.Request) -> web.Response: + hass = request.app[KEY_HASS] + # Clear all sessions + hass.auth.session._temp_sessions.clear() + hass.auth.session._strict_connection_sessions.clear() + + if request.query["token"] == "refresh": + await hass.auth.session.async_create_session(request, refresh_token) + else: + await hass.auth.session.async_create_temp_unauthorized_session(request) + session = await get_session(request) + return web.Response(text=session[SESSION_ID]) + + app.router.add_get("/test/cookie", set_cookie) + + +async def _test_strict_connection_non_cloud_enabled_setup( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + strict_connection_mode: StrictConnectionMode, +) -> tuple[TestClient, Callable[[str], None], RefreshToken]: + """Test external unauthenticated requests with strict connection non cloud enabled.""" + refresh_token = hass.auth.async_validate_access_token(hass_access_token) + assert refresh_token + session = hass.auth.session + assert session._strict_connection_sessions == {} + assert session._temp_sessions == {} + + _add_set_cookie_endpoint(app, refresh_token) + await async_setup_auth(hass, app, strict_connection_mode) + set_mock_ip = mock_real_ip(app) + client = await aiohttp_client(app) + return (client, set_mock_ip, refresh_token) + + +async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test external unauthenticated requests with strict connection non cloud enabled.""" + client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup( + hass, app, aiohttp_client, hass_access_token, strict_connection_mode + ) + + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test external unauthenticated requests with strict connection non cloud enabled and refresh token cookie.""" + ( + client, + set_mock_ip, + refresh_token, + ) = await _test_strict_connection_non_cloud_enabled_setup( + hass, app, aiohttp_client, hass_access_token, strict_connection_mode + ) + session = hass.auth.session + + # set strict connection cookie with refresh token + set_mock_ip(LOCALHOST_ADDRESSES[0]) + session_id = await (await client.get("/test/cookie?token=refresh")).text() + assert session._strict_connection_sessions == {session_id: refresh_token.id} + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + req = await client.get("/") + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": False} + + # Invalidate refresh token, which should also invalidate session + hass.auth.async_remove_refresh_token(refresh_token) + assert session._strict_connection_sessions == {} + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test external unauthenticated requests with strict connection non cloud enabled and temp cookie.""" + client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup( + hass, app, aiohttp_client, hass_access_token, strict_connection_mode + ) + session = hass.auth.session + + # set strict connection cookie with temp session + assert session._temp_sessions == {} + set_mock_ip(LOCALHOST_ADDRESSES[0]) + session_id = await (await client.get("/test/cookie?token=temp")).text() + assert client.session.cookie_jar.filter_cookies(URL("http://127.0.0.1")) + assert session_id in session._temp_sessions + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get("/") + assert resp.status == HTTPStatus.OK + assert await resp.json() == {"authenticated": False} + + async_fire_time_changed(hass, utcnow() + TEMP_TIMEOUT + timedelta(minutes=1)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert session._temp_sessions == {} + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + await perform_unauthenticated_request(hass, client) + + +async def _drop_connection_unauthorized_request( + _: HomeAssistant, client: TestClient +) -> None: + with pytest.raises(ServerDisconnectedError): + # unauthorized requests should raise ServerDisconnectedError + await client.get("/") + + +async def _guard_page_unauthorized_request( + hass: HomeAssistant, client: TestClient +) -> None: + req = await client.get("/") + assert req.status == HTTPStatus.IM_A_TEAPOT + + def read_guard_page() -> str: + with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file: + return file.read() + + assert await req.text() == await hass.async_add_executor_job(read_guard_page) + + +@pytest.mark.parametrize( + "test_func", + [ + _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests, + _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token, + _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session, + ], + ids=[ + "no cookie", + "refresh token cookie", + "temp session cookie", + ], +) +@pytest.mark.parametrize( + ("strict_connection_mode", "request_func"), + [ + (StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request), + (StrictConnectionMode.GUARD_PAGE, _guard_page_unauthorized_request), + ], + ids=["drop connection", "static page"], +) +async def test_strict_connection_non_cloud_external_unauthenticated_requests( + hass: HomeAssistant, + app_strict_connection: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + test_func: Callable[ + [ + HomeAssistant, + web.Application, + ClientSessionGenerator, + str, + Callable[[HomeAssistant, TestClient], Awaitable[None]], + StrictConnectionMode, + ], + Awaitable[None], + ], + strict_connection_mode: StrictConnectionMode, + request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]], +) -> None: + """Test external unauthenticated requests with strict connection non cloud.""" + await test_func( + hass, + app_strict_connection, + aiohttp_client, + hass_access_token, + request_func, + strict_connection_mode, + ) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index a10aa740268..41f36dad2df 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -10,7 +10,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_middlewares import middleware import pytest -import homeassistant.components.http as http +from homeassistant.components import http from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS from homeassistant.components.http.ban import ( IP_BANS_FILE, @@ -190,6 +190,7 @@ async def test_ip_ban_manager_never_started( BANNED_IPS_WITH_SUPERVISOR, [1, 1, 0], [HTTPStatus.FORBIDDEN, HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED], + strict=False, ) ), ) diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index af55e0b8597..9a4e80052f6 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -30,8 +30,7 @@ async def get_client(aiohttp_client, validator): return b"" TestView().register(app[KEY_HASS], app, app.router) - client = await aiohttp_client(app) - return client + return await aiohttp_client(app) async def test_validator(aiohttp_client: ClientSessionGenerator) -> None: diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 98e97d0fe57..9e576e10f4d 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -7,14 +7,18 @@ from ipaddress import ip_network import logging from pathlib import Path from unittest.mock import Mock, patch +from urllib.parse import quote_plus import pytest from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) -import homeassistant.components.http as http +from homeassistant.components import http +from homeassistant.components.http.const import StrictConnectionMode +from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.http import KEY_HASS from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component @@ -521,3 +525,80 @@ async def test_logging( response = await client.get("/api/states/logging.entity") assert response.status == HTTPStatus.OK assert "GET /api/states/logging.entity" not in caplog.text + + +@pytest.mark.skip(reason="Remove strict connection config option") +async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( + hass: HomeAssistant, +) -> None: + """Test service create_temporary_strict_connection_url with strict_connection not enabled.""" + assert await async_setup_component(hass, http.DOMAIN, {"http": {}}) + with pytest.raises( + ServiceValidationError, + match="Strict connection is not enabled for non-cloud requests", + ): + await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + +@pytest.mark.skip(reason="Remove strict connection config option") +@pytest.mark.parametrize( + ("mode"), + [ + StrictConnectionMode.DROP_CONNECTION, + StrictConnectionMode.GUARD_PAGE, + ], +) +async def test_service_create_temporary_strict_connection( + hass: HomeAssistant, mode: StrictConnectionMode +) -> None: + """Test service create_temporary_strict_connection_url.""" + assert await async_setup_component( + hass, http.DOMAIN, {"http": {"strict_connection": mode}} + ) + + # No external url set + assert hass.config.external_url is None + assert hass.config.internal_url is None + with pytest.raises(ServiceValidationError, match="No external URL available"): + await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + # Raise if only internal url is available + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + with pytest.raises(ServiceValidationError, match="No external URL available"): + await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + # Set external url too + external_url = "https://example.com" + await async_process_ha_core_config( + hass, + {"external_url": external_url}, + ) + assert hass.config.external_url == external_url + response = await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + assert isinstance(response, dict) + direct_url_prefix = f"{external_url}/auth/strict_connection/temp_token?authSig=" + assert response.pop("direct_url").startswith(direct_url_prefix) + assert response.pop("url").startswith( + f"https://login.home-assistant.io?u={quote_plus(direct_url_prefix)}" + ) + assert response == {} # No more keys in response diff --git a/tests/components/http/test_session.py b/tests/components/http/test_session.py new file mode 100644 index 00000000000..ae62365749a --- /dev/null +++ b/tests/components/http/test_session.py @@ -0,0 +1,107 @@ +"""Tests for HTTP session.""" + +from collections.abc import Callable +import logging +from typing import Any +from unittest.mock import patch + +from aiohttp import web +from aiohttp.test_utils import make_mocked_request +import pytest + +from homeassistant.auth.session import SESSION_ID +from homeassistant.components.http.session import ( + COOKIE_NAME, + HomeAssistantCookieStorage, +) +from homeassistant.core import HomeAssistant + + +def fake_request_with_strict_connection_cookie(cookie_value: str) -> web.Request: + """Return a fake request with a strict connection cookie.""" + request = make_mocked_request( + "GET", "/", headers={"Cookie": f"{COOKIE_NAME}={cookie_value}"} + ) + assert COOKIE_NAME in request.cookies + return request + + +@pytest.fixture +def cookie_storage(hass: HomeAssistant) -> HomeAssistantCookieStorage: + """Fixture for the cookie storage.""" + return HomeAssistantCookieStorage(hass) + + +def _encrypt_cookie_data(cookie_storage: HomeAssistantCookieStorage, data: Any) -> str: + """Encrypt cookie data.""" + cookie_data = cookie_storage._encoder(data).encode("utf-8") + return cookie_storage._fernet.encrypt(cookie_data).decode("utf-8") + + +@pytest.mark.parametrize( + "func", + [ + lambda _: "invalid", + lambda storage: _encrypt_cookie_data(storage, "bla"), + lambda storage: _encrypt_cookie_data(storage, None), + ], +) +async def test_load_session_modified_cookies( + cookie_storage: HomeAssistantCookieStorage, + caplog: pytest.LogCaptureFixture, + func: Callable[[HomeAssistantCookieStorage], str], +) -> None: + """Test that on modified cookies the session is empty and the request will be logged for ban.""" + request = fake_request_with_strict_connection_cookie(func(cookie_storage)) + with patch( + "homeassistant.components.http.session.process_wrong_login", + ) as mock_process_wrong_login: + session = await cookie_storage.load_session(request) + assert session.empty + assert ( + "homeassistant.components.http.session", + logging.WARNING, + "Cannot decrypt/parse cookie value", + ) in caplog.record_tuples + mock_process_wrong_login.assert_called() + + +async def test_load_session_validate_session( + hass: HomeAssistant, + cookie_storage: HomeAssistantCookieStorage, +) -> None: + """Test load session validates the session.""" + session = await cookie_storage.new_session() + session[SESSION_ID] = "bla" + request = fake_request_with_strict_connection_cookie( + _encrypt_cookie_data(cookie_storage, cookie_storage._get_session_data(session)) + ) + + with patch.object( + hass.auth.session, "async_validate_strict_connection_session", return_value=True + ) as mock_validate: + session = await cookie_storage.load_session(request) + assert not session.empty + assert session[SESSION_ID] == "bla" + mock_validate.assert_called_with(session) + + # verify lru_cache is working + mock_validate.reset_mock() + await cookie_storage.load_session(request) + mock_validate.assert_not_called() + + session = await cookie_storage.new_session() + session[SESSION_ID] = "something" + request = fake_request_with_strict_connection_cookie( + _encrypt_cookie_data(cookie_storage, cookie_storage._get_session_data(session)) + ) + + with patch.object( + hass.auth.session, + "async_validate_strict_connection_session", + return_value=False, + ): + session = await cookie_storage.load_session(request) + assert session.empty + assert SESSION_ID not in session + assert session._changed diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index f8ddaa42ac1..200796c87e7 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -12,7 +12,7 @@ from requests.exceptions import ConnectionError import requests_mock from requests_mock import ANY -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.huawei_lte.const import CONF_UNAUTHENTICATED_MODE, DOMAIN from homeassistant.const import ( @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -48,7 +49,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -63,7 +64,7 @@ async def test_urlize_plain_host( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert user_input[CONF_URL] == f"http://{host}/" @@ -96,7 +97,7 @@ async def test_already_configured( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +128,7 @@ async def test_connection_errors( data=FIXTURE_USER_INPUT | data_patch, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -219,7 +220,7 @@ async def test_login_error( data={**FIXTURE_USER_INPUT, **fixture_override}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -250,7 +251,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == user_input[CONF_URL] assert result["data"][CONF_USERNAME] == user_input[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == user_input[CONF_PASSWORD] @@ -270,7 +271,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> ssdp.ATTR_UPNP_SERIAL: "00000000", }, { - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "step_id": "user", "errors": {}, }, @@ -286,7 +287,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> # No ssdp.ATTR_UPNP_SERIAL }, { - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "step_id": "user", "errors": {}, }, @@ -301,7 +302,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> # Does not matter }, { - "type": data_entry_flow.FlowResultType.ABORT, + "type": FlowResultType.ABORT, "reason": "unsupported_device", }, ), @@ -351,7 +352,7 @@ async def test_ssdp( ( "OK", { - "type": data_entry_flow.FlowResultType.ABORT, + "type": FlowResultType.ABORT, "reason": "reauth_successful", }, FIXTURE_USER_INPUT, @@ -359,7 +360,7 @@ async def test_ssdp( ( f"{LoginErrorEnum.PASSWORD_WRONG}", { - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "errors": {CONF_PASSWORD: "incorrect_password"}, "step_id": "reauth_confirm", }, @@ -393,7 +394,7 @@ async def test_reauth( DOMAIN, context=context, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["data_schema"] is not None assert result["data_schema"]({}) == { @@ -431,7 +432,7 @@ async def test_options(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" recipient = "+15555550000" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 74cceb03aba..692bd1405cf 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components import zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -35,7 +36,10 @@ def create_mock_api_discovery(aioclient_mock, bridges): """Patch aiohttp responses with fake data for bridge discovery.""" aioclient_mock.get( URL_NUPNP, - json=[{"internalipaddress": host, "id": id} for (host, id) in bridges], + json=[ + {"internalipaddress": host, "id": bridge_id} + for (host, bridge_id) in bridges + ], ) for host, bridge_id in bridges: aioclient_mock.get( @@ -61,14 +65,14 @@ async def test_flow_works(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"id": disc_bridge.id} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" flow = next( @@ -83,7 +87,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hue Bridge aabbccddeeff" assert result["data"] == { "host": "1.2.3.4", @@ -108,14 +112,14 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"id": "manual"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" with patch.object(config_flow, "discover_bridge", return_value=disc_bridge): @@ -123,7 +127,7 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: result["flow_id"], {"host": "2.2.2.2"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with ( @@ -132,7 +136,7 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Hue Bridge {disc_bridge.id}" assert result["data"] == { "host": "2.2.2.2", @@ -159,14 +163,14 @@ async def test_manual_flow_bridge_exist(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "2.2.2.2"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -179,7 +183,7 @@ async def test_manual_flow_no_discovered_bridges( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -199,7 +203,7 @@ async def test_flow_all_discovered_bridges_exist( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -219,7 +223,7 @@ async def test_flow_bridges_discovered( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with pytest.raises(vol.Invalid): @@ -243,7 +247,7 @@ async def test_flow_two_bridges_discovered_one_new( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({"id": "beer"}) assert result["data_schema"]({"id": "manual"}) @@ -261,7 +265,7 @@ async def test_flow_timeout_discovery(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -285,7 +289,7 @@ async def test_flow_link_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "linking"} @@ -310,7 +314,7 @@ async def test_flow_link_button_not_pressed(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "register_failed"} @@ -335,7 +339,7 @@ async def test_flow_link_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -350,7 +354,7 @@ async def test_import_with_no_config( data={"host": "0.0.0.0"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -385,7 +389,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge( context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with ( @@ -397,7 +401,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge( ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hue Bridge id-1234" assert result["data"] == { "host": "2.2.2.2", @@ -431,7 +435,7 @@ async def test_bridge_homekit( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" flow = next( @@ -454,7 +458,7 @@ async def test_bridge_import_already_configured(hass: HomeAssistant) -> None: data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -481,7 +485,7 @@ async def test_bridge_homekit_already_configured( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -496,7 +500,7 @@ async def test_options_flow_v1(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert ( @@ -516,7 +520,7 @@ async def test_options_flow_v1(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { const.CONF_ALLOW_HUE_GROUPS: True, const.CONF_ALLOW_UNREACHABLE: True, @@ -550,7 +554,7 @@ async def test_options_flow_v2( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, const.CONF_IGNORE_AVAILABILITY) == [] @@ -560,7 +564,7 @@ async def test_options_flow_v2( user_input={const.CONF_IGNORE_AVAILABILITY: [mock_dev_id]}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { const.CONF_IGNORE_AVAILABILITY: [mock_dev_id], } @@ -589,7 +593,7 @@ async def test_bridge_zeroconf( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -625,7 +629,7 @@ async def test_bridge_zeroconf_already_exists( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "192.168.1.217" @@ -650,7 +654,7 @@ async def test_bridge_zeroconf_ipv6(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -676,7 +680,7 @@ async def test_bridge_connection_failed( # a warning message should have been logged that the bridge could not be reached assert "Error while attempting to retrieve discovery information" in caplog.text - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # test again with zeroconf discovered wrong bridge IP @@ -697,7 +701,7 @@ async def test_bridge_connection_failed( }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # test again with homekit discovered wrong bridge IP @@ -714,7 +718,7 @@ async def test_bridge_connection_failed( type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # repeat test with import flow @@ -723,5 +727,5 @@ async def test_bridge_connection_failed( context={"source": config_entries.SOURCE_IMPORT}, data={"host": "blah"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 0c65d425d4d..bf595737f29 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -8,9 +8,10 @@ from energyflip import ( EnergyFlipUnauthenticatedException, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.huisbaasje.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -49,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert form_result["type"] == "create_entry" + assert form_result["type"] is FlowResultType.CREATE_ENTRY assert form_result["title"] == "test-username" assert form_result["data"] == { "id": "test-id", @@ -80,7 +81,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -102,7 +103,7 @@ async def test_form_authenticate_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -124,7 +125,7 @@ async def test_form_authenticate_unknown_error(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -149,7 +150,7 @@ async def test_form_customer_overview_cannot_connect(hass: HomeAssistant) -> Non }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -174,7 +175,7 @@ async def test_form_customer_overview_authentication_error(hass: HomeAssistant) }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -199,7 +200,7 @@ async def test_form_customer_overview_unknown_error(hass: HomeAssistant) -> None }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -240,5 +241,5 @@ async def test_form_entry_exists(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.ABORT + assert form_result["type"] is FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index 13c41fd8369..567be27721f 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_action from homeassistant.const import STATE_ON, EntityCategory diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index fa17d1bb732..14ed9fae5e0 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_condition from homeassistant.const import ATTR_MODE, STATE_OFF, STATE_ON, EntityCategory @@ -187,8 +187,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -206,8 +208,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 3e05f6b02d1..fd6441588c4 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -6,7 +6,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_trigger from homeassistant.const import ( @@ -293,15 +293,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -317,15 +314,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -341,15 +335,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index ac4f6368f38..b9721f4adb1 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -37,7 +37,7 @@ async def test_user_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Powerview Generation {api_version}" assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result2["result"].unique_id == "A1B2C3D4E5G6H7" @@ -47,14 +47,14 @@ async def test_user_form( result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {} result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {CONF_HOST: "1.2.3.4"}, ) - assert result4["type"] == FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -84,7 +84,7 @@ async def test_form_homekit_and_dhcp_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # test we can recover from the failed entry @@ -97,7 +97,7 @@ async def test_form_homekit_and_dhcp_cannot_connect( result3 = await hass.config_entries.flow.async_configure(result2["flow_id"], {}) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" @@ -127,7 +127,7 @@ async def test_form_homekit_and_dhcp( data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] is None assert result["description_placeholders"] == { @@ -139,7 +139,7 @@ async def test_form_homekit_and_dhcp( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Powerview Generation {api_version}" assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result2["result"].unique_id == "A1B2C3D4E5G6H7" @@ -151,7 +151,7 @@ async def test_form_homekit_and_dhcp( context={"source": source}, data=discovery_info, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT @pytest.mark.usefixtures("mock_hunterdouglas_hub") @@ -178,7 +178,7 @@ async def test_discovered_by_homekit_and_dhcp( data=homekit_discovery, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_init( @@ -187,7 +187,7 @@ async def test_discovered_by_homekit_and_dhcp( data=dhcp_discovery, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -213,7 +213,7 @@ async def test_form_cannot_connect( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} # Now try again without the patch in place to make sure we can recover @@ -222,7 +222,7 @@ async def test_form_cannot_connect( {CONF_HOST: "1.2.3.4"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" @@ -257,7 +257,7 @@ async def test_form_no_data( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} # Now try again without the patch in place to make sure we can recover @@ -266,7 +266,7 @@ async def test_form_no_data( {CONF_HOST: "1.2.3.4"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" @@ -296,7 +296,7 @@ async def test_form_unknown_exception( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} # Now try again without the patch in place to make sure we can recover @@ -305,7 +305,7 @@ async def test_form_unknown_exception( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Powerview Generation {api_version}" assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result2["result"].unique_id == "A1B2C3D4E5G6H7" @@ -335,7 +335,7 @@ async def test_form_unsupported_device( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unsupported_device"} # Now try again without the patch in place to make sure we can recover @@ -344,7 +344,7 @@ async def test_form_unsupported_device( {CONF_HOST: "1.2.3.4"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr index aea65005fc4..ee951986062 100644 --- a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -50,12 +50,14 @@ 'activity': 'PARKED_IN_CS', 'error_code': 0, 'error_datetime': None, + 'error_datetime_naive': None, 'error_key': None, 'mode': 'MAIN_AREA', 'state': 'RESTRICTED', }), 'planner': dict({ - 'next_start_datetime': '2023-06-05T19:00:00', + 'next_start_datetime': '2023-06-05T19:00:00+00:00', + 'next_start_datetime_naive': '2023-06-05T19:00:00', 'override': dict({ 'action': 'NOT_ACTIVE', }), diff --git a/tests/components/husqvarna_automower/snapshots/test_number.ambr b/tests/components/husqvarna_automower/snapshots/test_number.ambr new file mode 100644 index 00000000000..a5479345bd1 --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_number.ambr @@ -0,0 +1,56 @@ +# serializer version: 1 +# name: test_snapshot_number[number.test_mower_1_cutting_height-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9, + 'min': 1, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.test_mower_1_cutting_height', + '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': 'Cutting height', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cutting_height', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_cutting_height', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_number[number.test_mower_1_cutting_height-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Cutting height', + 'max': 9, + 'min': 1, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.test_mower_1_cutting_height', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index ce81098f753..7d4533afe72 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -104,6 +104,344 @@ 'state': '0.034', }) # --- +# name: test_sensor[sensor.test_mower_1_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_error', + 'alarm_mower_in_motion', + 'alarm_mower_lifted', + 'alarm_mower_stopped', + 'alarm_mower_switched_off', + 'alarm_mower_tilted', + 'alarm_outside_geofence', + 'angular_sensor_problem', + 'battery_problem', + 'battery_problem', + 'battery_restriction_due_to_ambient_temperature', + 'can_error', + 'charging_current_too_high', + 'charging_station_blocked', + 'charging_system_problem', + 'charging_system_problem', + 'collision_sensor_defect', + 'collision_sensor_error', + 'collision_sensor_problem_front', + 'collision_sensor_problem_rear', + 'com_board_not_available', + 'communication_circuit_board_sw_must_be_updated', + 'complex_working_area', + 'connection_changed', + 'connection_not_changed', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_settings_restored', + 'cutting_drive_motor_1_defect', + 'cutting_drive_motor_2_defect', + 'cutting_drive_motor_3_defect', + 'cutting_height_blocked', + 'cutting_height_problem', + 'cutting_height_problem_curr', + 'cutting_height_problem_dir', + 'cutting_height_problem_drive', + 'cutting_motor_problem', + 'cutting_stopped_slope_too_steep', + 'cutting_system_blocked', + 'cutting_system_blocked', + 'cutting_system_imbalance_warning', + 'cutting_system_major_imbalance', + 'destination_not_reachable', + 'difficult_finding_home', + 'docking_sensor_defect', + 'electronic_problem', + 'empty_battery', + 'folding_cutting_deck_sensor_defect', + 'folding_sensor_activated', + 'geofence_problem', + 'geofence_problem', + 'gps_navigation_problem', + 'guide_1_not_found', + 'guide_2_not_found', + 'guide_3_not_found', + 'guide_calibration_accomplished', + 'guide_calibration_failed', + 'high_charging_power_loss', + 'high_internal_power_loss', + 'high_internal_temperature', + 'internal_voltage_error', + 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_sub_device_combination', + 'invalid_system_configuration', + 'left_brush_motor_overloaded', + 'lift_sensor_defect', + 'lifted', + 'limited_cutting_height_range', + 'limited_cutting_height_range', + 'loop_sensor_defect', + 'loop_sensor_problem_front', + 'loop_sensor_problem_left', + 'loop_sensor_problem_rear', + 'loop_sensor_problem_right', + 'low_battery', + 'memory_circuit_problem', + 'mower_lifted', + 'mower_tilted', + 'no_accurate_position_from_satellites', + 'no_confirmed_position', + 'no_drive', + 'no_loop_signal', + 'no_power_in_charging_station', + 'no_response_from_charger', + 'outside_working_area', + 'poor_signal_quality', + 'reference_station_communication_problem', + 'right_brush_motor_overloaded', + 'safety_function_faulty', + 'settings_restored', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_not_found', + 'sim_card_requires_pin', + 'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern', + 'slope_too_steep', + 'sms_could_not_be_sent', + 'stop_button_problem', + 'stuck_in_charging_station', + 'switch_cord_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'tilt_sensor_problem', + 'too_high_discharge_current', + 'too_high_internal_current', + 'trapped', + 'ultrasonic_problem', + 'ultrasonic_sensor_1_defect', + 'ultrasonic_sensor_2_defect', + 'ultrasonic_sensor_3_defect', + 'ultrasonic_sensor_4_defect', + 'unexpected_cutting_height_adj', + 'unexpected_error', + 'upside_down', + 'weak_gps_signal', + 'wheel_drive_problem_left', + 'wheel_drive_problem_rear_left', + 'wheel_drive_problem_rear_right', + 'wheel_drive_problem_right', + 'wheel_motor_blocked_left', + 'wheel_motor_blocked_rear_left', + 'wheel_motor_blocked_rear_right', + 'wheel_motor_blocked_right', + 'wheel_motor_overloaded_left', + 'wheel_motor_overloaded_rear_left', + 'wheel_motor_overloaded_rear_right', + 'wheel_motor_overloaded_right', + 'work_area_not_valid', + 'wrong_loop_signal', + 'wrong_pin_code', + 'zone_generator_problem', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_mower_1_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Error', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'error', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_mower_1_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Mower 1 Error', + 'options': list([ + 'no_error', + 'alarm_mower_in_motion', + 'alarm_mower_lifted', + 'alarm_mower_stopped', + 'alarm_mower_switched_off', + 'alarm_mower_tilted', + 'alarm_outside_geofence', + 'angular_sensor_problem', + 'battery_problem', + 'battery_problem', + 'battery_restriction_due_to_ambient_temperature', + 'can_error', + 'charging_current_too_high', + 'charging_station_blocked', + 'charging_system_problem', + 'charging_system_problem', + 'collision_sensor_defect', + 'collision_sensor_error', + 'collision_sensor_problem_front', + 'collision_sensor_problem_rear', + 'com_board_not_available', + 'communication_circuit_board_sw_must_be_updated', + 'complex_working_area', + 'connection_changed', + 'connection_not_changed', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_settings_restored', + 'cutting_drive_motor_1_defect', + 'cutting_drive_motor_2_defect', + 'cutting_drive_motor_3_defect', + 'cutting_height_blocked', + 'cutting_height_problem', + 'cutting_height_problem_curr', + 'cutting_height_problem_dir', + 'cutting_height_problem_drive', + 'cutting_motor_problem', + 'cutting_stopped_slope_too_steep', + 'cutting_system_blocked', + 'cutting_system_blocked', + 'cutting_system_imbalance_warning', + 'cutting_system_major_imbalance', + 'destination_not_reachable', + 'difficult_finding_home', + 'docking_sensor_defect', + 'electronic_problem', + 'empty_battery', + 'folding_cutting_deck_sensor_defect', + 'folding_sensor_activated', + 'geofence_problem', + 'geofence_problem', + 'gps_navigation_problem', + 'guide_1_not_found', + 'guide_2_not_found', + 'guide_3_not_found', + 'guide_calibration_accomplished', + 'guide_calibration_failed', + 'high_charging_power_loss', + 'high_internal_power_loss', + 'high_internal_temperature', + 'internal_voltage_error', + 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_sub_device_combination', + 'invalid_system_configuration', + 'left_brush_motor_overloaded', + 'lift_sensor_defect', + 'lifted', + 'limited_cutting_height_range', + 'limited_cutting_height_range', + 'loop_sensor_defect', + 'loop_sensor_problem_front', + 'loop_sensor_problem_left', + 'loop_sensor_problem_rear', + 'loop_sensor_problem_right', + 'low_battery', + 'memory_circuit_problem', + 'mower_lifted', + 'mower_tilted', + 'no_accurate_position_from_satellites', + 'no_confirmed_position', + 'no_drive', + 'no_loop_signal', + 'no_power_in_charging_station', + 'no_response_from_charger', + 'outside_working_area', + 'poor_signal_quality', + 'reference_station_communication_problem', + 'right_brush_motor_overloaded', + 'safety_function_faulty', + 'settings_restored', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_not_found', + 'sim_card_requires_pin', + 'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern', + 'slope_too_steep', + 'sms_could_not_be_sent', + 'stop_button_problem', + 'stuck_in_charging_station', + 'switch_cord_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'tilt_sensor_problem', + 'too_high_discharge_current', + 'too_high_internal_current', + 'trapped', + 'ultrasonic_problem', + 'ultrasonic_sensor_1_defect', + 'ultrasonic_sensor_2_defect', + 'ultrasonic_sensor_3_defect', + 'ultrasonic_sensor_4_defect', + 'unexpected_cutting_height_adj', + 'unexpected_error', + 'upside_down', + 'weak_gps_signal', + 'wheel_drive_problem_left', + 'wheel_drive_problem_rear_left', + 'wheel_drive_problem_rear_right', + 'wheel_drive_problem_right', + 'wheel_motor_blocked_left', + 'wheel_motor_blocked_rear_left', + 'wheel_motor_blocked_rear_right', + 'wheel_motor_blocked_right', + 'wheel_motor_overloaded_left', + 'wheel_motor_overloaded_rear_left', + 'wheel_motor_overloaded_rear_right', + 'wheel_motor_overloaded_right', + 'work_area_not_valid', + 'wrong_loop_signal', + 'wrong_pin_code', + 'zone_generator_problem', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_error', + }) +# --- # name: test_sensor[sensor.test_mower_1_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -210,7 +548,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2023-06-06T02:00:00+00:00', + 'state': '2023-06-05T19:00:00+00:00', }) # --- # name: test_sensor[sensor.test_mower_1_number_of_charging_cycles-entry] @@ -311,6 +649,78 @@ 'state': '11396', }) # --- +# name: test_sensor[sensor.test_mower_1_restricted_reason-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'all_work_areas_completed', + 'daily_limit', + 'external', + 'fota', + 'frost', + 'none', + 'not_applicable', + 'park_override', + 'sensor', + 'week_schedule', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_mower_1_restricted_reason', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restricted reason', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'restricted_reason', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_restricted_reason', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_mower_1_restricted_reason-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Mower 1 Restricted reason', + 'options': list([ + 'all_work_areas_completed', + 'daily_limit', + 'external', + 'fota', + 'frost', + 'none', + 'not_applicable', + 'park_override', + 'sensor', + 'week_schedule', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_restricted_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'week_schedule', + }) +# --- # name: test_sensor[sensor.test_mower_1_total_charging_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py index 425636ba915..5500b547853 100644 --- a/tests/components/husqvarna_automower/test_binary_sensor.py +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests for binary sensor platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.model import MowerActivities @@ -9,6 +8,7 @@ from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -20,6 +20,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_value_fixture, + snapshot_platform, ) @@ -51,7 +52,7 @@ async def test_binary_sensor_states( ]: values[TEST_MOWER_ID].mower.activity = activity mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(f"binary_sensor.{entity}") @@ -71,13 +72,6 @@ async def test_snapshot_binary_sensor( [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 + await snapshot_platform( + hass, entity_registry, snapshot, 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 e22ab7718ec..0a345eed627 100644 --- a/tests/components/husqvarna_automower/test_config_flow.py +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -98,7 +98,7 @@ async def test_config_non_unique_profile( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -125,7 +125,7 @@ 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["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +190,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert mock_config_entry.unique_id == USER_ID @@ -261,7 +261,7 @@ async def test_reauth_wrong_account( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "wrong_account" assert mock_config_entry.unique_id == USER_ID diff --git a/tests/components/husqvarna_automower/test_device_tracker.py b/tests/components/husqvarna_automower/test_device_tracker.py index d9cab0d5074..015be201ccc 100644 --- a/tests/components/husqvarna_automower/test_device_tracker.py +++ b/tests/components/husqvarna_automower/test_device_tracker.py @@ -10,7 +10,7 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform async def test_device_tracker_snapshot( @@ -26,13 +26,6 @@ async def test_device_tracker_snapshot( [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 + await snapshot_platform( + hass, entity_registry, snapshot, 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_init.py b/tests/components/husqvarna_automower/test_init.py index 3c97a3b2668..dbf1d429eee 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -31,12 +31,12 @@ async def test_load_unload_entry( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -87,7 +87,7 @@ async def test_update_failed( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_websocket_not_available( @@ -105,13 +105,13 @@ async def test_websocket_not_available( assert "Failed to connect to websocket. Trying to reconnect: Boom" in caplog.text assert mock_automower_client.auth.websocket_connect.call_count == 1 assert mock_automower_client.start_listening.call_count == 1 - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED freezer.tick(timedelta(seconds=2)) async_fire_time_changed(hass) await hass.async_block_till_done() 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 + assert mock_config_entry.state is ConfigEntryState.LOADED async def test_device_info( diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 6e491fd4a28..c8aea0e7c98 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -1,6 +1,5 @@ """Tests for lawn_mower module.""" -from datetime import timedelta from unittest.mock import AsyncMock from aioautomower.exceptions import ApiException @@ -9,6 +8,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.components.lawn_mower import LawnMowerActivity from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -46,7 +46,7 @@ async def test_lawn_mower_states( values[TEST_MOWER_ID].mower.activity = activity values[TEST_MOWER_ID].mower.state = state mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("lawn_mower.test_mower_1") diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py new file mode 100644 index 00000000000..b66f1965151 --- /dev/null +++ b/tests/components/husqvarna_automower/test_number.py @@ -0,0 +1,70 @@ +"""Tests for number platform.""" + +from unittest.mock import AsyncMock, patch + +from aioautomower.exceptions import ApiException +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_number_commands( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test number commands.""" + entity_id = "number.test_mower_1_cutting_height" + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + domain="number", + service="set_value", + target={"entity_id": entity_id}, + service_data={"value": "3"}, + blocking=True, + ) + mocked_method = mock_automower_client.set_cutting_height + 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="number", + service="set_value", + target={"entity_id": entity_id}, + service_data={"value": "3"}, + 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 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_snapshot_number( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test states of the number entity.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.NUMBER], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py index 4283c7d3797..9e255eb410f 100644 --- a/tests/components/husqvarna_automower/test_select.py +++ b/tests/components/husqvarna_automower/test_select.py @@ -1,6 +1,5 @@ """Tests for select platform.""" -from datetime import timedelta from unittest.mock import AsyncMock from aioautomower.exceptions import ApiException @@ -10,6 +9,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -48,7 +48,7 @@ async def test_select_states( ]: values[TEST_MOWER_ID].headlight.mode = state mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("select.test_mower_1_headlight_mode") diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index bc464b2ce78..2c0661f82cb 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -1,6 +1,5 @@ """Tests for sensor platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.model import MowerModes @@ -10,7 +9,8 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN -from homeassistant.const import Platform +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -21,6 +21,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_value_fixture, + snapshot_platform, ) @@ -41,11 +42,11 @@ async def test_sensor_unknown_states( values[TEST_MOWER_ID].mower.mode = MowerModes.UNKNOWN mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.test_mower_1_mode") - assert state.state == "unknown" + assert state.state == STATE_UNKNOWN async def test_cutting_blade_usage_time_sensor( @@ -62,6 +63,30 @@ async def test_cutting_blade_usage_time_sensor( assert state.state == "0.034" +async def test_next_start_sensor( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test if this sensor is only added, if data is available.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("sensor.test_mower_1_next_start") + assert state is not None + assert state.state == "2023-06-05T19:00:00+00:00" + + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].planner.next_start_datetime = None + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_next_start") + assert state.state == STATE_UNKNOWN + + @pytest.mark.parametrize( ("sensor_to_test"), [ @@ -94,6 +119,31 @@ async def test_statistics_not_available( assert state is None +async def test_error_sensor( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test error sensor.""" + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + + for state, expected_state in [ + (None, "no_error"), + ("can_error", "can_error"), + ]: + values[TEST_MOWER_ID].mower.error_key = state + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_error") + assert state.state == expected_state + + async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -107,13 +157,6 @@ async def test_sensor( [Platform.SENSOR], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, 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_switch.py b/tests/components/husqvarna_automower/test_switch.py index 22137a35323..aab1128a746 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -1,6 +1,5 @@ """Tests for switch platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ApiException @@ -11,6 +10,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -23,6 +23,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_value_fixture, + snapshot_platform, ) @@ -45,7 +46,7 @@ async def test_switch_states( values[TEST_MOWER_ID].mower.state = state values[TEST_MOWER_ID].planner.restricted_reason = restricted_reson mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("switch.test_mower_1_enable_schedule") @@ -106,13 +107,6 @@ async def test_switch( [Platform.SWITCH], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, 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/huum/test_config_flow.py b/tests/components/huum/test_config_flow.py index 219783079e3..9917f71fc08 100644 --- a/tests/components/huum/test_config_flow.py +++ b/tests/components/huum/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -89,7 +89,7 @@ async def test_signup_flow_already_set_up(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT @pytest.mark.parametrize( @@ -122,7 +122,7 @@ async def test_huum_errors( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_base} with ( @@ -142,4 +142,4 @@ async def test_huum_errors( CONF_PASSWORD: TEST_PASSWORD, }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 2712e1bbca9..d9545b903c1 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from pygti.exceptions import CannotConnect, InvalidAuth -from homeassistant import data_entry_flow from homeassistant.components.hvv_departures.const import ( CONF_FILTER, CONF_REAL_TIME, @@ -15,6 +14,7 @@ from homeassistant.components.hvv_departures.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_fixture @@ -77,7 +77,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: {CONF_STATION: "Wartenau"}, ) - assert result_station_select["type"] == "create_entry" + assert result_station_select["type"] is FlowResultType.CREATE_ENTRY assert result_station_select["title"] == "Wartenau" assert result_station_select["data"] == { CONF_HOST: "api-test.geofox.de", @@ -159,7 +159,7 @@ async def test_user_flow_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result_user["type"] == "form" + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "invalid_auth"} @@ -181,7 +181,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result_user["type"] == "form" + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "cannot_connect"} @@ -217,7 +217,7 @@ async def test_user_flow_station(hass: HomeAssistant) -> None: result_user["flow_id"], None, ) - assert result_station["type"] == "form" + assert result_station["type"] is FlowResultType.FORM assert result_station["step_id"] == "station" @@ -255,7 +255,7 @@ async def test_user_flow_station_select(hass: HomeAssistant) -> None: None, ) - assert result_station_select["type"] == "form" + assert result_station_select["type"] is FlowResultType.FORM assert result_station_select["step_id"] == "station_select" @@ -289,7 +289,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -297,7 +297,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_FILTER: ["0"], CONF_OFFSET: 15, CONF_REAL_TIME: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_FILTER: [ { @@ -349,7 +349,7 @@ async def test_options_flow_invalid_auth(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -388,7 +388,7 @@ async def test_options_flow_cannot_connect(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 8e22fbe84f7..11670cb3565 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -15,7 +15,7 @@ from pydrawise.schema import ( import pytest from homeassistant.components.hydrawise.const import DOMAIN -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -32,7 +32,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_pydrawise( +def mock_legacy_pydrawise( user: User, controller: Controller, zones: list[Zone], @@ -47,10 +47,32 @@ def mock_pydrawise( yield mock_pydrawise.return_value +@pytest.fixture +def mock_pydrawise( + mock_auth: AsyncMock, + user: User, + controller: Controller, + zones: list[Zone], +) -> Generator[AsyncMock, None, None]: + """Mock Hydrawise.""" + with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise: + user.controllers = [controller] + controller.zones = zones + mock_pydrawise.return_value.get_user.return_value = user + yield mock_pydrawise.return_value + + +@pytest.fixture +def mock_auth() -> Generator[AsyncMock, None, None]: + """Mock pydrawise Auth.""" + with patch("pydrawise.auth.Auth", autospec=True) as mock_auth: + yield mock_auth.return_value + + @pytest.fixture def user() -> User: """Hydrawise User fixture.""" - return User(customer_id=12345) + return User(customer_id=12345, email="asdf@asdf.com") @pytest.fixture @@ -102,7 +124,7 @@ def zones() -> list[Zone]: @pytest.fixture -def mock_config_entry() -> MockConfigEntry: +def mock_config_entry_legacy() -> MockConfigEntry: """Mock ConfigEntry.""" return MockConfigEntry( title="Hydrawise", @@ -111,6 +133,23 @@ def mock_config_entry() -> MockConfigEntry: CONF_API_KEY: "abc123", }, unique_id="hydrawise-customerid", + version=1, + ) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock ConfigEntry.""" + return MockConfigEntry( + title="Hydrawise", + domain=DOMAIN, + data={ + CONF_USERNAME: "asfd@asdf.com", + CONF_PASSWORD: "__password__", + }, + unique_id="hydrawise-customerid", + version=1, + minor_version=2, ) diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index 17c3eda1699..a7fbc008aab 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -3,15 +3,15 @@ from unittest.mock import AsyncMock from aiohttp import ClientError +from pydrawise.exceptions import NotAuthorizedError from pydrawise.schema import User import pytest from homeassistant import config_entries from homeassistant.components.hydrawise.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -import homeassistant.helpers.issue_registry as ir from tests.common import MockConfigEntry @@ -28,21 +28,25 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": "abc123"} + result["flow_id"], + {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}, ) mock_pydrawise.get_user.return_value = user await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Hydrawise" - assert result2["data"] == {"api_key": "abc123"} + assert result2["data"] == { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + } assert len(mock_setup_entry.mock_calls) == 1 - mock_pydrawise.get_user.assert_called_once_with(fetch_zones=False) + mock_pydrawise.get_user.assert_called_once_with() async def test_form_api_error( @@ -54,17 +58,17 @@ async def test_form_api_error( init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {"api_key": "abc123"} + data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} mock_pydrawise.get_user.reset_mock(side_effect=True) mock_pydrawise.get_user.return_value = user result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_connect_timeout( @@ -75,100 +79,48 @@ async def test_form_connect_timeout( init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {"api_key": "abc123"} + data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout_connect"} mock_pydrawise.get_user.reset_mock(side_effect=True) mock_pydrawise.get_user.return_value = user result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY -async def test_flow_import_success( +async def test_form_not_authorized_error( hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User ) -> None: - """Test that we can import a YAML config.""" - mock_pydrawise.get_user.return_value = User - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, + """Test we handle API errors.""" + mock_pydrawise.get_user.side_effect = NotAuthorizedError + + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Hydrawise" - assert result["data"] == { - CONF_API_KEY: "__api_key__", - } - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" + data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} + result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], data ) - assert issue.translation_key == "deprecated_yaml" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_pydrawise.get_user.reset_mock(side_effect=True) + mock_pydrawise.get_user.return_value = user + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result2["type"] is FlowResultType.CREATE_ENTRY -async def test_flow_import_api_error( - hass: HomeAssistant, mock_pydrawise: AsyncMock +async def test_reauth( + hass: HomeAssistant, + user: User, + mock_pydrawise: AsyncMock, ) -> None: - """Test that we handle API errors on YAML import.""" - mock_pydrawise.get_user.side_effect = ClientError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) - await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - DOMAIN, "deprecated_yaml_import_issue_cannot_connect" - ) - assert issue.translation_key == "deprecated_yaml_import_issue" - - -async def test_flow_import_connect_timeout( - hass: HomeAssistant, mock_pydrawise: AsyncMock -) -> None: - """Test that we handle connection timeouts on YAML import.""" - mock_pydrawise.get_user.side_effect = TimeoutError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) - await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "timeout_connect" - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - DOMAIN, "deprecated_yaml_import_issue_timeout_connect" - ) - assert issue.translation_key == "deprecated_yaml_import_issue" - - -async def test_flow_import_already_imported( - hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User -) -> None: - """Test that we can handle a YAML config already imported.""" + """Test that re-authorization works.""" mock_config_entry = MockConfigEntry( title="Hydrawise", domain=DOMAIN, @@ -179,23 +131,20 @@ async def test_flow_import_already_imported( ) mock_config_entry.add_to_hass(hass) - mock_pydrawise.get_user.return_value = user - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) + mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT - assert result.get("reason") == "already_configured" + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + [result] = flows + assert result["step_id"] == "user" - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}, ) - assert issue.translation_key == "deprecated_yaml" + mock_pydrawise.get_user.return_value = user + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/hydrawise/test_init.py b/tests/components/hydrawise/test_init.py index 6b41867b044..8ec3c3da648 100644 --- a/tests/components/hydrawise/test_init.py +++ b/tests/components/hydrawise/test_init.py @@ -5,29 +5,11 @@ from unittest.mock import AsyncMock from aiohttp import ClientError from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -import homeassistant.helpers.issue_registry as ir -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_setup_import_success( - hass: HomeAssistant, mock_pydrawise: AsyncMock -) -> None: - """Test that setup with a YAML config triggers an import and warning.""" - config = {"hydrawise": {CONF_ACCESS_TOKEN: "_access-token_"}} - assert await async_setup_component(hass, "hydrawise", config) - await hass.async_block_till_done() - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" - ) - assert issue.translation_key == "deprecated_yaml" - - async def test_connect_retry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: AsyncMock ) -> None: @@ -37,3 +19,16 @@ async def test_connect_retry( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_update_version( + hass: HomeAssistant, mock_config_entry_legacy: MockConfigEntry +) -> None: + """Test updating to the GaphQL API works.""" + mock_config_entry_legacy.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_legacy.entry_id) + await hass.async_block_till_done() + assert mock_config_entry_legacy.state is ConfigEntryState.SETUP_ERROR + + # Make sure reauth flow has been initiated + assert any(mock_config_entry_legacy.async_get_active_flows(hass, {"reauth"})) diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 86dc4c5c39d..57749f5eedc 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -10,7 +10,6 @@ from unittest.mock import AsyncMock, Mock, patch from hyperion import const -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.hyperion.const import ( CONF_AUTH_ID, @@ -30,7 +29,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, FlowResultType from . import ( TEST_AUTH_REQUIRED_RESP, @@ -165,7 +164,7 @@ async def test_user_if_no_configuration(hass: HomeAssistant) -> None: """Check flow behavior when no configuration is present.""" result = await _init_flow(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == DOMAIN @@ -180,7 +179,7 @@ async def test_user_existing_id_abort(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -196,7 +195,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Fail the auth check call. @@ -206,7 +205,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -225,7 +224,7 @@ async def test_user_confirm_cannot_connect(hass: HomeAssistant) -> None: side_effect=[good_client, bad_client], ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -241,7 +240,7 @@ async def test_user_confirm_id_error(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_id" @@ -255,7 +254,7 @@ async def test_user_noauth_flow_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -274,7 +273,7 @@ async def test_user_auth_required(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -288,7 +287,7 @@ async def test_auth_static_token_auth_required_fail(hass: HomeAssistant) -> None "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -308,7 +307,7 @@ async def test_auth_static_token_success(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -334,7 +333,7 @@ async def test_auth_static_token_login_connect_fail(hass: HomeAssistant) -> None hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -357,7 +356,7 @@ async def test_auth_static_token_login_fail(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "invalid_access_token" @@ -372,7 +371,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -390,7 +389,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, @@ -398,13 +397,13 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_granted_error" @@ -439,7 +438,7 @@ async def test_auth_create_token_approval_declined_task_canceled( mock_task = CanceledAwaitableMock() task_coro: Awaitable | None = None - def create_task(arg: Any) -> CanceledAwaitableMock: + def create_task(arg: Any, **kwargs: Any) -> CanceledAwaitableMock: nonlocal task_coro task_coro = arg return mock_task @@ -459,6 +458,7 @@ async def test_auth_create_token_approval_declined_task_canceled( ) assert result["step_id"] == "create_token" + # Tests should not patch the async_create_task function with patch.object(hass, "async_create_task", side_effect=create_task): result = await _configure_flow(hass, result) assert result["step_id"] == "create_token_external" @@ -486,7 +486,7 @@ async def test_auth_create_token_when_issued_token_fails( "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -503,14 +503,14 @@ async def test_auth_create_token_when_issued_token_fails( result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. @@ -519,7 +519,7 @@ async def test_auth_create_token_when_issued_token_fails( client.async_client_connect = AsyncMock(return_value=False) result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -534,7 +534,7 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -551,19 +551,19 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -613,7 +613,7 @@ async def test_auth_create_token_success_but_login_fail( # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_work_error" @@ -633,7 +633,7 @@ async def test_ssdp_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -654,7 +654,7 @@ async def test_ssdp_cannot_connect(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -672,7 +672,7 @@ async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_id" @@ -691,7 +691,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON @@ -716,7 +716,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: ): result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -725,7 +725,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" # Verify a working URL is used despite the bad port number @@ -749,8 +749,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result_1["type"] == data_entry_flow.FlowResultType.FORM - assert result_2["type"] == data_entry_flow.FlowResultType.ABORT + assert result_1["type"] is FlowResultType.FORM + assert result_2["type"] is FlowResultType.ABORT assert result_2["reason"] == "already_in_progress" @@ -768,7 +768,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_priority = 1 @@ -777,7 +777,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: user_input={CONF_PRIORITY: new_priority}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PRIORITY] == new_priority # Turn the light on and ensure the new priority is used. @@ -810,14 +810,14 @@ async def test_options_effect_show_list(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # effect1 and effect3 only, so effect2 is hidden. assert result["data"][CONF_EFFECT_HIDE_LIST] == ["effect2"] @@ -838,7 +838,7 @@ async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> N client.async_client_connect = AsyncMock(return_value=False) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -867,13 +867,13 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert CONF_TOKEN in config_entry.data @@ -899,5 +899,5 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index 816f03efa9e..d20200a1457 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -2,10 +2,11 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ialarm.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -20,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA["host"] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +63,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -80,7 +81,7 @@ async def test_form_exception(hass: HomeAssistant) -> None: result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -106,5 +107,5 @@ async def test_form_already_exists(hass: HomeAssistant) -> None: result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 64a09e218c3..4aaa66416f6 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -9,6 +9,7 @@ from iaqualink.exception import ( from homeassistant.components.iaqualink import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_already_configured( @@ -23,7 +24,7 @@ async def test_already_configured( result = await flow.async_step_user(config_data) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT async def test_without_config(hass: HomeAssistant) -> None: @@ -34,7 +35,7 @@ async def test_without_config(hass: HomeAssistant) -> None: result = await flow.async_step_user() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -50,7 +51,7 @@ async def test_with_invalid_credentials(hass: HomeAssistant, config_data) -> Non ): result = await flow.async_step_user(config_data) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -66,7 +67,7 @@ async def test_service_exception(hass: HomeAssistant, config_data) -> None: ): result = await flow.async_step_user(config_data) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -83,6 +84,6 @@ async def test_with_existing_config(hass: HomeAssistant, config_data) -> None: ): result = await flow.async_step_user(config_data) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == config_data["username"] assert result["data"] == config_data diff --git a/tests/components/iaqualink/test_utils.py b/tests/components/iaqualink/test_utils.py index c803fb48b09..b9aba93523c 100644 --- a/tests/components/iaqualink/test_utils.py +++ b/tests/components/iaqualink/test_utils.py @@ -15,9 +15,10 @@ async def test_await_or_reraise(hass: HomeAssistant) -> None: async_noop = async_returns(None) await await_or_reraise(async_noop()) - with pytest.raises(Exception): - async_ex = async_raises(Exception) + with pytest.raises(Exception) as exc_info: + async_ex = async_raises(Exception("Test exception")) await await_or_reraise(async_ex()) + assert str(exc_info.value) == "Test exception" with pytest.raises(HomeAssistantError): async_ex = async_raises(AqualinkServiceException) diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py index 3a3e1d90d91..0833508d03f 100644 --- a/tests/components/ibeacon/test_config_flow.py +++ b/tests/components/ibeacon/test_config_flow.py @@ -18,7 +18,7 @@ async def test_setup_user_no_bluetooth( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bluetooth_not_available" @@ -28,13 +28,13 @@ async def test_setup_user(hass: HomeAssistant, enable_bluetooth: None) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.ibeacon.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "iBeacon Tracker" assert result2["data"] == {} @@ -48,7 +48,7 @@ async def test_setup_user_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -62,7 +62,7 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test save invalid uuid @@ -72,7 +72,7 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None "new_uuid": "invalid", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"new_uuid": "invalid_uuid_format"} @@ -84,13 +84,13 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None "new_uuid": uuid, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_NAMELESS_UUIDS: [uuid]} # test save duplicate uuid result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -100,13 +100,13 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None "new_uuid": uuid, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_NAMELESS_UUIDS: [uuid]} # delete result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -115,5 +115,5 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None CONF_ALLOW_NAMELESS_UUIDS: [], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_NAMELESS_UUIDS: []} diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index f13a0e14595..ec8d11f1135 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, Mock, patch from pyicloud.exceptions import PyiCloudFailedLoginException import pytest -from homeassistant import data_entry_flow from homeassistant.components.icloud.config_flow import ( CONF_TRUSTED_DEVICE, CONF_VERIFICATION_CODE, @@ -22,6 +21,7 @@ from homeassistant.components.icloud.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_CONFIG, @@ -159,7 +159,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with required @@ -168,7 +168,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -186,7 +186,7 @@ async def test_user_with_cookie( CONF_WITH_FAMILY: WITH_FAMILY, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -207,7 +207,7 @@ async def test_login_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -220,7 +220,7 @@ async def test_no_device( context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_device" @@ -233,7 +233,7 @@ async def test_trusted_device(hass: HomeAssistant, service: MagicMock) -> None: ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -248,7 +248,7 @@ async def test_trusted_device_success(hass: HomeAssistant, service: MagicMock) - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -265,7 +265,7 @@ async def test_send_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} @@ -282,7 +282,7 @@ async def test_verification_code(hass: HomeAssistant, service: MagicMock) -> Non ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -303,7 +303,7 @@ async def test_verification_code_success( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -329,7 +329,7 @@ async def test_validate_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {"base": "validate_verification_code"} @@ -348,7 +348,7 @@ async def test_2fa_code_success(hass: HomeAssistant, service_2fa: MagicMock) -> result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -372,7 +372,7 @@ async def test_validate_2fa_code_failed( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE assert result["errors"] == {"base": "validate_verification_code"} @@ -392,13 +392,13 @@ async def test_password_update( data={**MOCK_CONFIG}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == PASSWORD_2 @@ -416,7 +416,7 @@ async def test_password_update_wrong_password(hass: HomeAssistant) -> None: data={**MOCK_CONFIG}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", @@ -426,5 +426,5 @@ async def test_password_update_wrong_password(hass: HomeAssistant) -> None: result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index 78eacfb6942..a861dc5f5e2 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -26,7 +26,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -46,7 +46,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == IDASEN_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -64,7 +64,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -85,7 +85,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -101,7 +101,7 @@ async def test_user_step_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -120,7 +120,7 @@ async def test_user_step_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -140,7 +140,7 @@ async def test_user_step_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == IDASEN_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -158,7 +158,7 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -177,7 +177,7 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "auth_failed"} @@ -197,7 +197,7 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == IDASEN_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -215,7 +215,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -236,7 +236,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -260,7 +260,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == IDASEN_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -276,7 +276,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IDASEN_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -298,7 +298,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == IDASEN_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 71bd2bc297f..44896dc0f2c 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,9 +1,10 @@ """Test the init file of IFTTT.""" -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ifttt from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from tests.typing import ClientSessionGenerator @@ -20,10 +21,10 @@ async def test_config_flow_registers_webhook( result = await hass.config_entries.flow.async_init( "ifttt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] ifttt_events = [] diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 6415e4e2a4e..62027552fb0 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, patch import pytest -import homeassistant.components.http as http +from homeassistant.components import http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import HomeAssistant diff --git a/tests/components/imap/conftest.py b/tests/components/imap/conftest.py index 74176efab11..dfe5fa2040f 100644 --- a/tests/components/imap/conftest.py +++ b/tests/components/imap/conftest.py @@ -1,6 +1,6 @@ """Fixtures for imap tests.""" -from collections.abc import Generator +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from aioimaplib import AUTH, LOGOUT, NONAUTH, SELECTED, STARTED, Response @@ -62,7 +62,7 @@ async def mock_imap_protocol( imap_pending_idle: bool, imap_login_state: str, imap_select_state: str, -) -> Generator[MagicMock, None]: +) -> AsyncGenerator[MagicMock, None]: """Mock the aioimaplib IMAP protocol handler.""" with patch( @@ -79,7 +79,14 @@ async def mock_imap_protocol( async def close() -> Response: """Mock imap close the selected folder.""" - imap_mock.protocol.state = imap_login_state + return Response("OK", []) + + async def store(uid: str, flags: str) -> Response: + """Mock imap store command.""" + return Response("OK", []) + + async def copy(uid: str, folder: str) -> Response: + """Mock imap store command.""" return Response("OK", []) async def logout() -> Response: @@ -101,12 +108,17 @@ async def mock_imap_protocol( imap_mock.has_pending_idle.return_value = imap_pending_idle imap_mock.protocol = MagicMock() imap_mock.protocol.state = STARTED + imap_mock.protocol.expunge = AsyncMock() + imap_mock.protocol.expunge.return_value = Response("OK", []) imap_mock.has_capability.return_value = imap_has_capability imap_mock.login.side_effect = login imap_mock.close.side_effect = close + imap_mock.copy.side_effect = copy imap_mock.logout.side_effect = logout imap_mock.select.side_effect = select imap_mock.search.return_value = Response(*imap_search) + imap_mock.store.side_effect = store imap_mock.fetch.return_value = Response(*imap_fetch) imap_mock.wait_hello_from_server.side_effect = wait_hello_from_server + imap_mock.timeout = 3 yield imap_mock diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index 9d9edae5b14..459cecec4a6 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -7,7 +7,7 @@ from aioimaplib import AioImapException import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.imap.const import ( CONF_CHARSET, CONF_FOLDER, @@ -29,11 +29,13 @@ MOCK_CONFIG = { "charset": "utf-8", "folder": "INBOX", "search": "UnSeen UnDeleted", + "event_message_data": ["text", "headers"], } MOCK_OPTIONS = { "folder": "INBOX", "search": "UnSeen UnDeleted", + "event_message_data": ["text", "headers"], } pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -44,7 +46,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -59,7 +61,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "email@email.com" assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -73,7 +75,7 @@ async def test_entry_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,7 +91,7 @@ async def test_entry_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -107,7 +109,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -138,7 +140,7 @@ async def test_form_cannot_connect( result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} # make sure we do not lose the user input if somethings gets wrong @@ -165,7 +167,7 @@ async def test_form_invalid_charset(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_CHARSET: "invalid_charset"} @@ -183,7 +185,7 @@ async def test_form_invalid_folder(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_FOLDER: "invalid_folder"} @@ -201,7 +203,7 @@ async def test_form_invalid_search(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_SEARCH: "invalid_search"} @@ -222,7 +224,7 @@ async def test_reauth_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {CONF_USERNAME: "email@email.com"} @@ -241,7 +243,7 @@ async def test_reauth_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -263,7 +265,7 @@ async def test_reauth_failed(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -277,7 +279,7 @@ async def test_reauth_failed(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -301,7 +303,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -315,7 +317,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -328,7 +330,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_config = MOCK_OPTIONS.copy() @@ -344,7 +346,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result["flow_id"], new_config ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_SEARCH: "invalid_search"} new_config["search"] = "UnSeen UnDeleted" @@ -358,7 +360,7 @@ async def test_options_form(hass: HomeAssistant) -> None: new_config, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == {} for key, value in new_config.items(): assert entry.data[key] == value @@ -381,7 +383,7 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: # so that it conflicts with that of entry1 result = await hass.config_entries.options.async_init(entry2.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_config = MOCK_OPTIONS.copy() @@ -395,26 +397,26 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: new_config, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @pytest.mark.parametrize( ("advanced_options", "assert_result"), [ - ({"max_message_size": 8192}, data_entry_flow.FlowResultType.CREATE_ENTRY), - ({"max_message_size": 1024}, data_entry_flow.FlowResultType.FORM), - ({"max_message_size": 65536}, data_entry_flow.FlowResultType.FORM), + ({"max_message_size": 8192}, FlowResultType.CREATE_ENTRY), + ({"max_message_size": 1024}, FlowResultType.FORM), + ({"max_message_size": 65536}, FlowResultType.FORM), ( {"custom_event_data_template": "{{ subject }}"}, - data_entry_flow.FlowResultType.CREATE_ENTRY, + FlowResultType.CREATE_ENTRY, ), ( {"custom_event_data_template": "{{ invalid_syntax"}, - data_entry_flow.FlowResultType.FORM, + FlowResultType.FORM, ), - ({"enable_push": True}, data_entry_flow.FlowResultType.CREATE_ENTRY), - ({"enable_push": False}, data_entry_flow.FlowResultType.CREATE_ENTRY), + ({"enable_push": True}, FlowResultType.CREATE_ENTRY), + ({"enable_push": False}, FlowResultType.CREATE_ENTRY), ], ids=[ "valid_message_size", @@ -429,7 +431,7 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: async def test_advanced_options_form( hass: HomeAssistant, advanced_options: dict[str, str], - assert_result: data_entry_flow.FlowResultType, + assert_result: FlowResultType, ) -> None: """Test we show the advanced options.""" @@ -442,7 +444,7 @@ async def test_advanced_options_form( context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_config = MOCK_OPTIONS.copy() @@ -460,14 +462,14 @@ async def test_advanced_options_form( assert result2["type"] == assert_result if result2.get("errors") is not None: - assert assert_result == data_entry_flow.FlowResultType.FORM + assert assert_result is FlowResultType.FORM else: # Check if entry was updated for key, value in new_config.items(): assert entry.data[key] == value except vol.Invalid: # Check if form was expected with these options - assert assert_result == data_entry_flow.FlowResultType.FORM + assert assert_result is FlowResultType.FORM @pytest.mark.parametrize("cipher_list", ["python_default", "modern", "intermediate"]) @@ -483,7 +485,7 @@ async def test_config_flow_with_cipherlist_and_ssl_verify( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -498,7 +500,39 @@ async def test_config_flow_with_cipherlist_and_ssl_verify( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "email@email.com" + assert result2["data"] == config + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize("event_message_data", [[], ["headers"], ["text", "headers"]]) +async def test_config_flow_with_event_message_data( + hass: HomeAssistant, mock_setup_entry: AsyncMock, event_message_data: list +) -> None: + """Test with different message data.""" + config = MOCK_CONFIG.copy() + config["event_message_data"] = event_message_data + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], config + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "email@email.com" assert result2["data"] == config assert len(mock_setup_entry.mock_calls) == 1 @@ -515,7 +549,7 @@ async def test_config_flow_from_with_advanced_settings( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -527,7 +561,7 @@ async def test_config_flow_from_with_advanced_settings( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "cannot_connect" assert "ssl_cipher_list" in result2["data_schema"].schema @@ -544,7 +578,7 @@ async def test_config_flow_from_with_advanced_settings( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "email@email.com" assert result3["data"] == config assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/imap/test_diagnostics.py b/tests/components/imap/test_diagnostics.py index 79d51b73401..721e09352f2 100644 --- a/tests/components/imap/test_diagnostics.py +++ b/tests/components/imap/test_diagnostics.py @@ -66,6 +66,10 @@ async def test_entry_diagnostics( "port": 993, "charset": "utf-8", "folder": "INBOX", + "event_message_data": [ + "text", + "headers", + ], "search": "UnSeen UnDeleted", "custom_event_data_template": "{{ 4 * 4 }}", } diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 8608963413a..a8f51142d8d 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -14,6 +14,7 @@ from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder from homeassistant.components.sensor.const import SensorStateClass from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.util.dt import utcnow from .const import ( @@ -164,6 +165,7 @@ async def test_receiving_message_successfully( assert data["folder"] == "INBOX" assert data["sender"] == "john.doe@example.com" assert data["subject"] == "Test subject" + assert data["uid"] == "1" assert "Test body" in data["text"] assert ( valid_date @@ -213,6 +215,7 @@ async def test_receiving_message_with_invalid_encoding( assert data["sender"] == "john.doe@example.com" assert data["subject"] == "Test subject" assert data["text"] == TEST_BADLY_ENCODED_CONTENT + assert data["uid"] == "1" @pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) @@ -251,6 +254,7 @@ async def test_receiving_message_no_subject_to_from( assert data["text"] == "Test body\r\n" assert data["headers"]["Return-Path"] == ("",) assert data["headers"]["Delivered-To"] == ("notify@example.com",) + assert data["uid"] == "1" @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) @@ -670,6 +674,41 @@ async def test_message_is_truncated( assert len(event_data["text"]) == 3 +@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) +@pytest.mark.parametrize( + "imap_fetch", [(TEST_FETCH_RESPONSE_TEXT_PLAIN)], ids=["plain"] +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +@pytest.mark.parametrize("event_message_data", [[], ["text"], ["text", "headers"]]) +async def test_message_data( + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + caplog: pytest.LogCaptureFixture, + event_message_data: list, +) -> None: + """Test with different message data.""" + event_called = async_capture_events(hass, "imap_content") + + config = MOCK_CONFIG.copy() + # Mock different message data + config["event_message_data"] = event_message_data + config_entry = MockConfigEntry(domain=DOMAIN, data=config) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # We should have received one message + assert state is not None + assert state.state == "1" + assert len(event_called) == 1 + + event_data = event_called[0].data + assert set(event_message_data).issubset(set(event_data)) + + @pytest.mark.parametrize( ("imap_search", "imap_fetch"), [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)], @@ -777,3 +816,160 @@ async def test_enforce_polling( mock_imap_protocol.wait_server_push.assert_not_called() else: mock_imap_protocol.assert_has_calls([call.wait_server_push]) + + +@pytest.mark.parametrize( + ("imap_search", "imap_fetch"), + [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)], +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> None: + """Test receiving a message successfully.""" + event_called = async_capture_events(hass, "imap_content") + + config = MOCK_CONFIG.copy() + config_entry = MockConfigEntry(domain=DOMAIN, data=config) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # we should have received one message + assert state is not None + assert state.state == "1" + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT + + # we should have received one event + assert len(event_called) == 1 + data: dict[str, Any] = event_called[0].data + assert data["server"] == "imap.server.com" + assert data["username"] == "email@email.com" + assert data["search"] == "UnSeen UnDeleted" + assert data["folder"] == "INBOX" + assert data["sender"] == "john.doe@example.com" + assert data["subject"] == "Test subject" + assert data["uid"] == "1" + assert data["entry_id"] == config_entry.entry_id + + # Test seen service + data = {"entry": config_entry.entry_id, "uid": "1"} + await hass.services.async_call(DOMAIN, "seen", data, blocking=True) + mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Seen)") + mock_imap_protocol.store.reset_mock() + + # Test move service + data = { + "entry": config_entry.entry_id, + "uid": "1", + "seen": True, + "target_folder": "Trash", + } + await hass.services.async_call(DOMAIN, "move", data, blocking=True) + mock_imap_protocol.store.assert_has_calls( + [call("1", "+FLAGS (\\Seen)"), call("1", "+FLAGS (\\Deleted)")] + ) + mock_imap_protocol.copy.assert_called_with("1", "Trash") + mock_imap_protocol.protocol.expunge.assert_called_once() + mock_imap_protocol.store.reset_mock() + mock_imap_protocol.copy.reset_mock() + mock_imap_protocol.protocol.expunge.reset_mock() + + # Test delete service + data = {"entry": config_entry.entry_id, "uid": "1"} + await hass.services.async_call(DOMAIN, "delete", data, blocking=True) + mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Deleted)") + mock_imap_protocol.protocol.expunge.assert_called_once() + + # Test fetch service + data = {"entry": config_entry.entry_id, "uid": "1"} + response = await hass.services.async_call( + DOMAIN, "fetch", data, blocking=True, return_response=True + ) + mock_imap_protocol.fetch.assert_called_with("1", "BODY.PEEK[]") + assert response["text"] == "Test body\r\n" + assert response["sender"] == "john.doe@example.com" + assert response["subject"] == "Test subject" + assert response["uid"] == "1" + + # Test with invalid entry_id + data = {"entry": "invalid", "uid": "1"} + with pytest.raises(ServiceValidationError) as exc: + await hass.services.async_call(DOMAIN, "seen", data, blocking=True) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "invalid_entry" + + # Test processing imap client failures + exceptions = { + "invalid_auth": {"exc": InvalidAuth(), "translation_placeholders": None}, + "invalid_folder": {"exc": InvalidFolder(), "translation_placeholders": None}, + "imap_server_fail": { + "exc": AioImapException("Bla"), + "translation_placeholders": {"error": "Bla"}, + }, + } + for translation_key, attrs in exceptions.items(): + with patch( + "homeassistant.components.imap.connect_to_server", side_effect=attrs["exc"] + ): + data = {"entry": config_entry.entry_id, "uid": "1"} + with pytest.raises(ServiceValidationError) as exc: + await hass.services.async_call(DOMAIN, "seen", data, blocking=True) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == translation_key + assert ( + exc.value.translation_placeholders == attrs["translation_placeholders"] + ) + + # Test unexpected errors with storing a flag during a service call + service_calls_response = { + "seen": ({"entry": config_entry.entry_id, "uid": "1"}, False), + "move": ( + { + "entry": config_entry.entry_id, + "uid": "1", + "seen": False, + "target_folder": "Trash", + }, + False, + ), + "delete": ({"entry": config_entry.entry_id, "uid": "1"}, False), + "fetch": ({"entry": config_entry.entry_id, "uid": "1"}, True), + } + patch_error_translation_key = { + "seen": ("store", "seen_failed"), + "move": ("copy", "copy_failed"), + "delete": ("store", "delete_failed"), + "fetch": ("fetch", "fetch_failed"), + } + for service, (data, response) in service_calls_response.items(): + with ( + pytest.raises(ServiceValidationError) as exc, + patch.object( + mock_imap_protocol, + patch_error_translation_key[service][0], + side_effect=AioImapException("Bla"), + ), + ): + await hass.services.async_call( + DOMAIN, service, data, blocking=True, return_response=response + ) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "imap_server_fail" + assert exc.value.translation_placeholders == {"error": "Bla"} + # Test with bad responses + with ( + pytest.raises(ServiceValidationError) as exc, + patch.object( + mock_imap_protocol, + patch_error_translation_key[service][0], + return_value=Response("BAD", [b"Bla"]), + ), + ): + await hass.services.async_call( + DOMAIN, service, data, blocking=True, return_response=response + ) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == patch_error_translation_key[service][1] + assert exc.value.translation_placeholders == {"error": "Bla"} diff --git a/tests/components/improv_ble/test_config_flow.py b/tests/components/improv_ble/test_config_flow.py index bafc32907ab..53da1f28425 100644 --- a/tests/components/improv_ble/test_config_flow.py +++ b/tests/components/improv_ble/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_step_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_user_step_success_authorize(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -83,7 +83,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -96,7 +96,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -107,7 +107,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await _test_common_success_wo_identify( hass, result, IMPROV_BLE_DISCOVERY_INFO.address @@ -124,7 +124,7 @@ async def test_bluetooth_step_provisioned_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=PROVISIONED_IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_provisioned" @@ -138,7 +138,7 @@ async def test_bluetooth_step_provisioned_device_2(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert len(hass.config_entries.flow.async_progress_by_handler("improv_ble")) == 1 @@ -156,7 +156,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -172,7 +172,7 @@ async def test_bluetooth_step_success_identify(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -192,7 +192,7 @@ async def _test_common_success_with_identify( result["flow_id"], {CONF_ADDRESS: address}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["identify", "provision"] assert result["step_id"] == "main_menu" @@ -201,12 +201,12 @@ async def _test_common_success_with_identify( result["flow_id"], {"next_step_id": "identify"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "identify" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["identify", "provision"] assert result["step_id"] == "main_menu" @@ -214,7 +214,7 @@ async def _test_common_success_with_identify( result["flow_id"], {"next_step_id": "provision"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] is None @@ -237,7 +237,7 @@ async def _test_common_success_wo_identify( result["flow_id"], {CONF_ADDRESS: address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] is None @@ -266,14 +266,14 @@ async def _test_common_success( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" assert result["step_id"] == "do_provision" await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("description_placeholders") == placeholders - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason mock_provision.assert_awaited_once_with("MyWIFI", "secret", None) @@ -290,7 +290,7 @@ async def _test_common_success_wo_identify_w_authorize( result["flow_id"], {CONF_ADDRESS: address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] is None @@ -321,7 +321,7 @@ async def _test_common_success_w_authorize( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "authorize" assert result["step_id"] == "authorize" mock_subscribe_state_updates.assert_awaited_once() @@ -338,14 +338,14 @@ async def _test_common_success_w_authorize( ) as mock_provision, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" assert result["step_id"] == "do_provision" await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["description_placeholders"] == {"url": "http://blabla.local"} - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "provision_successful_url" mock_provision.assert_awaited_once_with("MyWIFI", "secret", None) @@ -358,7 +358,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -366,7 +366,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -385,12 +385,12 @@ async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -401,7 +401,7 @@ async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -420,12 +420,12 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -436,7 +436,7 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" with patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.identify", side_effect=exc): @@ -444,7 +444,7 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {"next_step_id": "identify"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -463,12 +463,12 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -479,7 +479,7 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" with patch( @@ -488,7 +488,7 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -507,12 +507,12 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -523,7 +523,7 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" with ( @@ -539,7 +539,7 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -550,12 +550,12 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -566,7 +566,7 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" with ( @@ -582,7 +582,7 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" assert result["step_id"] == "do_provision" await hass.async_block_till_done() @@ -604,7 +604,7 @@ async def test_provision_fails(hass: HomeAssistant, exc, error) -> None: flow_id = await _test_provision_error(hass, exc) result = await hass.config_entries.flow.async_configure(flow_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -627,7 +627,7 @@ async def test_provision_not_authorized(hass: HomeAssistant, exc, error) -> None ): flow_id = await _test_provision_error(hass, exc) result = await hass.config_entries.flow.async_configure(flow_id) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "authorize" assert result["step_id"] == "authorize" @@ -646,6 +646,6 @@ async def test_provision_retry(hass: HomeAssistant, exc, error) -> None: flow_id = await _test_provision_error(hass, exc) result = await hass.config_entries.flow.async_configure(flow_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] == {"base": error} diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 41f14aa78e3..9d672b7ceb0 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -3,11 +3,12 @@ from dataclasses import dataclass import datetime from http import HTTPStatus +import logging from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest -import homeassistant.components.influxdb as influxdb +from homeassistant.components import influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON, STATE_STANDBY from homeassistant.core import HomeAssistant, split_entity_id @@ -1574,10 +1575,23 @@ async def test_invalid_inputs_error( write_api = get_write_api(mock_client) write_api.side_effect = test_exception - with patch(f"{INFLUX_PATH}.time.sleep") as sleep: + log_emit_done = hass.loop.create_future() + + original_emit = caplog.handler.emit + + def wait_for_emit(record: logging.LogRecord) -> None: + original_emit(record) + if record.levelname == "ERROR": + hass.loop.call_soon_threadsafe(log_emit_done.set_result, None) + + with ( + patch(f"{INFLUX_PATH}.time.sleep") as sleep, + patch.object(caplog.handler, "emit", wait_for_emit), + ): hass.states.async_set("fake.something", 1) await hass.async_block_till_done() await async_wait_for_queue_to_process(hass) + await log_emit_done await hass.async_block_till_done() write_api.assert_called_once() diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 395d33004a7..d3464c7e417 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -12,6 +12,7 @@ from influxdb_client.rest import ApiException import pytest from voluptuous import Invalid +from homeassistant.components import sensor from homeassistant.components.influxdb.const import ( API_VERSION_2, DEFAULT_API_VERSION, @@ -22,7 +23,6 @@ from homeassistant.components.influxdb.const import ( TEST_QUERY_V2, ) from homeassistant.components.influxdb.sensor import PLATFORM_SCHEMA -import homeassistant.components.sensor as sensor from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import PLATFORM_NOT_READY_BASE_WAIT_TIME diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index ffb25ebd093..154132c34fc 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IBBQ_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "iBBQ AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_not_inkbird(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_INKBIRD_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -48,7 +48,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -62,14 +62,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -85,7 +85,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -157,7 +157,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -170,7 +170,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -181,14 +181,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/insteon/mock_setup.py b/tests/components/insteon/mock_setup.py new file mode 100644 index 00000000000..c0d90509a50 --- /dev/null +++ b/tests/components/insteon/mock_setup.py @@ -0,0 +1,44 @@ +"""Utility to setup the Insteon integration.""" + +from homeassistant.components.insteon.api import async_load_api +from homeassistant.components.insteon.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator + + +async def async_mock_setup( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_data: dict | None = None, + config_options: dict | None = None, +): + """Set up for tests.""" + config_data = MOCK_USER_INPUT_PLM if config_data is None else config_data + config_options = {} if config_options is None else config_options + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data=config_data, + options=config_options, + ) + config_entry.add_to_hass(hass) + async_load_api(hass) + + ws_client = await hass_ws_client(hass) + devices = MockDevices() + await devices.async_load() + + dev_reg = dr.async_get(hass) + # Create device registry entry for mock node + ha_device = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "11.11.11")}, + name="Device 11.11.11", + ) + return ws_client, devices, ha_device, dev_reg diff --git a/tests/components/insteon/test_api_config.py b/tests/components/insteon/test_api_config.py new file mode 100644 index 00000000000..7c922338638 --- /dev/null +++ b/tests/components/insteon/test_api_config.py @@ -0,0 +1,391 @@ +"""Test the Insteon APIs for configuring the integration.""" + +from unittest.mock import patch + +from homeassistant.components.insteon.api.device import ID, TYPE +from homeassistant.components.insteon.const import ( + CONF_HUB_VERSION, + CONF_OVERRIDE, + CONF_X10, +) +from homeassistant.core import HomeAssistant + +from .const import ( + MOCK_DEVICE, + MOCK_HOSTNAME, + MOCK_USER_INPUT_HUB_V1, + MOCK_USER_INPUT_HUB_V2, + MOCK_USER_INPUT_PLM, +) +from .mock_connection import mock_failed_connection, mock_successful_connection +from .mock_setup import async_mock_setup + +from tests.typing import WebSocketGenerator + + +class MockProtocol: + """A mock Insteon protocol object.""" + + connected = True + + +async def test_get_config( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon configuration.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json({ID: 2, TYPE: "insteon/config/get"}) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["modem_config"] == {"device": MOCK_DEVICE} + + +async def test_get_modem_schema_plm( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon PLM modem configuration schema.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json({ID: 2, TYPE: "insteon/config/get_modem_schema"}) + msg = await ws_client.receive_json() + result = msg["result"][0] + + assert result["default"] == MOCK_DEVICE + assert result["name"] == "device" + assert result["required"] + + +async def test_get_modem_schema_hub( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon PLM modem configuration schema.""" + + ws_client, devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + ) + await ws_client.send_json({ID: 2, TYPE: "insteon/config/get_modem_schema"}) + msg = await ws_client.receive_json() + result = msg["result"][0] + + assert result["default"] == MOCK_HOSTNAME + assert result["name"] == "host" + assert result["required"] + + +async def test_update_modem_config_plm( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon PLM modem configuration schema.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_successful_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_PLM, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["status"] == "success" + + +async def test_update_modem_config_hub_v2( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon HubV2 modem configuration schema.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + config_options={"dev_path": "/some/path"}, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_successful_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_HUB_V2, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["status"] == "success" + + +async def test_update_modem_config_hub_v1( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon HubV1 modem configuration schema.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_data={**MOCK_USER_INPUT_HUB_V1, CONF_HUB_VERSION: 1}, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_successful_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_HUB_V1, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["status"] == "success" + + +async def test_update_modem_config_bad( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test updating the Insteon modem configuration with bad connection information.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_failed_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_PLM, + } + ) + msg = await ws_client.receive_json() + result = msg["error"] + assert result["code"] == "connection_failed" + + +async def test_update_modem_config_bad_reconnect( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test updating the Insteon modem configuration with bad connection information so reconnect to old.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_failed_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + mock_devices.modem.protocol = MockProtocol() + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_PLM, + } + ) + msg = await ws_client.receive_json() + result = msg["error"] + assert result["code"] == "connection_failed" + + +async def test_add_device_override( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a device configuration override.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + await ws_client.send_json( + {ID: 2, TYPE: "insteon/config/device_override/add", "override": override} + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + assert config_entry.options[CONF_OVERRIDE][0]["address"] == "99.99.99" + + +async def test_add_device_override_duplicate( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a duplicate device configuration override.""" + + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_OVERRIDE: [override]} + ) + await ws_client.send_json( + {ID: 2, TYPE: "insteon/config/device_override/add", "override": override} + ) + msg = await ws_client.receive_json() + assert msg["error"] + + +async def test_remove_device_override( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing a device configuration override.""" + + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + overrides = [ + override, + { + "address": "88.88.88", + "cat": "0x02", + "subcat": "0x05", + }, + ] + + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_OVERRIDE: overrides} + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/device_override/remove", + "device_address": "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + assert config_entry.options[CONF_OVERRIDE][0]["address"] == "88.88.88" + + +async def test_add_device_override_with_x10( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a device configuration override when X10 configuration exists.""" + + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_X10: [x10_device]} + ) + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + await ws_client.send_json( + {ID: 2, TYPE: "insteon/config/device_override/add", "override": override} + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + + +async def test_remove_device_override_with_x10( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing a device configuration override when X10 configuration exists.""" + + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + overrides = [ + override, + { + "address": "88.88.88", + "cat": "0x02", + "subcat": "0x05", + }, + ] + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + + ws_client, _, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_options={CONF_OVERRIDE: overrides, CONF_X10: [x10_device]}, + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/device_override/remove", + "device_address": "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + + +async def test_remove_device_override_no_overrides( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing a device override when no overrides are configured.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/device_override/remove", + "device_address": "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert not config_entry.options.get(CONF_OVERRIDE) diff --git a/tests/components/insteon/test_api_device.py b/tests/components/insteon/test_api_device.py index f3c67d479d0..29d601eb3ef 100644 --- a/tests/components/insteon/test_api_device.py +++ b/tests/components/insteon/test_api_device.py @@ -18,48 +18,29 @@ from homeassistant.components.insteon.api.device import ( TYPE, async_device_name, ) -from homeassistant.components.insteon.const import DOMAIN, MULTIPLE +from homeassistant.components.insteon.const import ( + CONF_OVERRIDE, + CONF_X10, + DOMAIN, + MULTIPLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import MOCK_USER_INPUT_PLM from .mock_devices import MockDevices +from .mock_setup import async_mock_setup from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator -async def _async_setup(hass, hass_ws_client): - """Set up for tests.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data=MOCK_USER_INPUT_PLM, - options={}, - ) - config_entry.add_to_hass(hass) - async_load_api(hass) - - ws_client = await hass_ws_client(hass) - devices = MockDevices() - await devices.async_load() - - dev_reg = dr.async_get(hass) - # Create device registry entry for mock node - ha_device = dev_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, "11.11.11")}, - name="Device 11.11.11", - ) - return ws_client, devices, ha_device, dev_reg - - -async def test_get_device_api( +async def test_get_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test getting an Insteon device.""" - ws_client, devices, ha_device, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, ha_device, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: ha_device.id} @@ -76,7 +57,7 @@ async def test_no_ha_device( ) -> None: """Test response when no HA device exists.""" - ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, _, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: "not_a_device"} @@ -141,7 +122,7 @@ async def test_get_ha_device_name( ) -> None: """Test getting the HA device name from an Insteon address.""" - _, devices, _, device_reg = await _async_setup(hass, hass_ws_client) + _, devices, _, device_reg = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): # Test a real HA and Insteon device @@ -164,7 +145,7 @@ async def test_add_device_api( ) -> None: """Test adding an Insteon device.""" - ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, _, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): await ws_client.send_json({ID: 2, TYPE: "insteon/device/add", MULTIPLE: True}) @@ -194,7 +175,7 @@ async def test_cancel_add_device( ) -> None: """Test cancelling adding of a new device.""" - ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, _, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.aldb, "devices", devices): await ws_client.send_json( @@ -205,3 +186,127 @@ async def test_cancel_add_device( ) msg = await ws_client.receive_json() assert msg["success"] + + +async def test_add_x10_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding an X10 device.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/add_x10", "x10_device": x10_device} + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + assert config_entry.options[CONF_X10][0]["housecode"] == "a" + assert config_entry.options[CONF_X10][0]["unitcode"] == 1 + assert config_entry.options[CONF_X10][0]["platform"] == "switch" + + +async def test_add_x10_device_duplicate( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a duplicate X10 device.""" + + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_X10: [x10_device]} + ) + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/add_x10", "x10_device": x10_device} + ) + msg = await ws_client.receive_json() + assert msg["error"] + assert msg["error"]["code"] == "duplicate" + + +async def test_remove_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing an Insteon device.""" + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "11.22.33", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + +async def test_remove_x10_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing an X10 device.""" + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "X10.A.01", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + +async def test_remove_one_x10_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test one X10 device without removing others.""" + x10_device = {"housecode": "a", "unitcode": 1, "platform": "light", "dim_steps": 22} + x10_devices = [ + x10_device, + {"housecode": "a", "unitcode": 2, "platform": "switch"}, + ] + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_X10: x10_devices} + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "X10.A.01", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + assert config_entry.options[CONF_X10][0]["housecode"] == "a" + assert config_entry.options[CONF_X10][0]["unitcode"] == 2 + + +async def test_remove_device_with_overload( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing an Insteon device that has a device overload.""" + overload = {"address": "99.99.99", "cat": 1, "subcat": 3} + overloads = {CONF_OVERRIDE: [overload]} + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options=overloads + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "99.99.99", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert not config_entry.options.get(CONF_OVERRIDE) diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index df7430bc254..4d3fb815463 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -5,41 +5,19 @@ from unittest.mock import patch import pytest from voluptuous_serialize import convert -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, usb from homeassistant.components.insteon.config_flow import ( - STEP_ADD_OVERRIDE, - STEP_ADD_X10, - STEP_CHANGE_HUB_CONFIG, - STEP_CHANGE_PLM_CONFIG, STEP_HUB_V1, STEP_HUB_V2, STEP_PLM, STEP_PLM_MANUALLY, - STEP_REMOVE_OVERRIDE, - STEP_REMOVE_X10, -) -from homeassistant.components.insteon.const import ( - CONF_CAT, - CONF_DIM_STEPS, - CONF_HOUSECODE, - CONF_HUB_VERSION, - CONF_OVERRIDE, - CONF_SUBCAT, - CONF_UNITCODE, - CONF_X10, - DOMAIN, -) -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_HOST, - CONF_PASSWORD, - CONF_PLATFORM, - CONF_PORT, - CONF_USERNAME, ) +from homeassistant.components.insteon.const import CONF_HUB_VERSION, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_DEVICE, @@ -50,11 +28,8 @@ from .const import ( PATCH_ASYNC_SETUP, PATCH_ASYNC_SETUP_ENTRY, PATCH_CONNECTION, - PATCH_CONNECTION_CLOSE, - PATCH_DEVICES, PATCH_USB_LIST, ) -from .mock_devices import MockDevices from tests.common import MockConfigEntry @@ -91,13 +66,12 @@ async def _init_form(hass, modem_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU - result2 = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": modem_type}, ) - return result2 async def _device_form(hass, flow_id, connection, user_input): @@ -123,7 +97,7 @@ async def test_form_select_modem(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_HUB_V2) assert result["step_id"] == STEP_HUB_V2 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_fail_on_existing(hass: HomeAssistant) -> None: @@ -135,14 +109,14 @@ async def test_fail_on_existing(hass: HomeAssistant) -> None: options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -154,7 +128,7 @@ async def test_form_select_plm(hass: HomeAssistant) -> None: result2, mock_setup, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_USER_INPUT_PLM assert len(mock_setup.mock_calls) == 1 @@ -172,7 +146,7 @@ async def test_form_select_plm_no_usb(hass: HomeAssistant) -> None: hass, result["flow_id"], mock_successful_connection, None ) USB_PORTS.update(temp_usb_list) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == STEP_PLM_MANUALLY @@ -188,8 +162,8 @@ async def test_form_select_plm_manual(hass: HomeAssistant) -> None: result3, mock_setup, mock_setup_entry = await _device_form( hass, result2["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM ) - assert result2["type"] == "form" - assert result3["type"] == "create_entry" + assert result2["type"] is FlowResultType.FORM + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == MOCK_USER_INPUT_PLM assert len(mock_setup.mock_calls) == 1 @@ -204,7 +178,7 @@ async def test_form_select_hub_v1(hass: HomeAssistant) -> None: result2, mock_setup, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V1 ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { **MOCK_USER_INPUT_HUB_V1, CONF_HUB_VERSION: 1, @@ -222,7 +196,7 @@ async def test_form_select_hub_v2(hass: HomeAssistant) -> None: result2, mock_setup, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V2 ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { **MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2, @@ -238,13 +212,13 @@ async def test_form_discovery_dhcp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": STEP_HUB_V2}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM schema = convert(result2["data_schema"]) found_host = False for field in schema: @@ -262,7 +236,7 @@ async def test_failed_connection_plm(hass: HomeAssistant) -> None: result2, _, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -277,7 +251,7 @@ async def test_failed_connection_plm_manually(hass: HomeAssistant) -> None: result3, _, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -289,384 +263,10 @@ async def test_failed_connection_hub(hass: HomeAssistant) -> None: result2, _, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_HUB_V2 ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} -async def _options_init_form(hass, entry_id, step): - """Run the init options form.""" - with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): - result = await hass.config_entries.options.async_init(entry_id) - - assert result["type"] == data_entry_flow.FlowResultType.MENU - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {"next_step_id": step}, - ) - return result2 - - -async def _options_form( - hass, flow_id, user_input, connection=mock_successful_connection -): - """Test an 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), - ): - result = await hass.config_entries.options.async_configure(flow_id, user_input) - return result, mock_setup_entry - - -async def test_options_change_hub_config(hass: HomeAssistant) -> None: - """Test changing Hub v2 config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_HUB_CONFIG - ) - - user_input = { - CONF_HOST: "2.3.4.5", - CONF_PORT: 9999, - CONF_USERNAME: "new username", - CONF_PASSWORD: "new password", - } - result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {} - assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} - - -async def test_options_change_hub_bad_config(hass: HomeAssistant) -> None: - """Test changing Hub v2 with bad config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_HUB_CONFIG - ) - - user_input = { - CONF_HOST: "2.3.4.5", - CONF_PORT: 9999, - CONF_USERNAME: "new username", - CONF_PASSWORD: "new password", - } - result, _ = await _options_form( - hass, result["flow_id"], user_input, mock_failed_connection - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == "cannot_connect" - - -async def test_options_change_plm_config(hass: HomeAssistant) -> None: - """Test changing PLM config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data=MOCK_USER_INPUT_PLM, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_PLM_CONFIG - ) - - user_input = {CONF_DEVICE: "/dev/ttyUSB0"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {} - assert config_entry.data == user_input - - -async def test_options_change_plm_bad_config(hass: HomeAssistant) -> None: - """Test changing PLM config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data=MOCK_USER_INPUT_PLM, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_PLM_CONFIG - ) - - user_input = {CONF_DEVICE: "/dev/ttyUSB0"} - result, _ = await _options_form( - hass, result["flow_id"], user_input, mock_failed_connection - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == "cannot_connect" - - -async def test_options_add_device_override(hass: HomeAssistant) -> None: - """Test adding a device override.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) - - user_input = { - CONF_ADDRESS: "1a2b3c", - CONF_CAT: "0x04", - CONF_SUBCAT: "0xaa", - } - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" - assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 - assert config_entry.options[CONF_OVERRIDE][0][CONF_SUBCAT] == 170 - - result2 = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) - - user_input = { - CONF_ADDRESS: "4d5e6f", - CONF_CAT: "05", - CONF_SUBCAT: "bb", - } - result3, _ = await _options_form(hass, result2["flow_id"], user_input) - - assert len(config_entry.options[CONF_OVERRIDE]) == 2 - assert config_entry.options[CONF_OVERRIDE][1][CONF_ADDRESS] == "4D.5E.6F" - assert config_entry.options[CONF_OVERRIDE][1][CONF_CAT] == 5 - assert config_entry.options[CONF_OVERRIDE][1][CONF_SUBCAT] == 187 - - # If result1 eq result2 the changes will not save - assert result["data"] != result3["data"] - - -async def test_options_remove_device_override(hass: HomeAssistant) -> None: - """Test removing a device override.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_OVERRIDE: [ - {CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 6, CONF_SUBCAT: 100}, - {CONF_ADDRESS: "4D.5E.6F", CONF_CAT: 7, CONF_SUBCAT: 200}, - ] - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_OVERRIDE) - - user_input = {CONF_ADDRESS: "1A.2B.3C"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - - -async def test_options_remove_device_override_with_x10(hass: HomeAssistant) -> None: - """Test removing a device override when an X10 device is configured.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_OVERRIDE: [ - {CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 6, CONF_SUBCAT: 100}, - {CONF_ADDRESS: "4D.5E.6F", CONF_CAT: 7, CONF_SUBCAT: 200}, - ], - CONF_X10: [ - { - CONF_HOUSECODE: "d", - CONF_UNITCODE: 5, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 22, - } - ], - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_OVERRIDE) - - user_input = {CONF_ADDRESS: "1A.2B.3C"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - assert len(config_entry.options[CONF_X10]) == 1 - - -async def test_options_add_x10_device(hass: HomeAssistant) -> None: - """Test adding an X10 device.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_X10) - - user_input = { - CONF_HOUSECODE: "c", - CONF_UNITCODE: 12, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 18, - } - result2, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 1 - assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" - assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 - assert config_entry.options[CONF_X10][0][CONF_PLATFORM] == "light" - assert config_entry.options[CONF_X10][0][CONF_DIM_STEPS] == 18 - - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_X10) - user_input = { - CONF_HOUSECODE: "d", - CONF_UNITCODE: 10, - CONF_PLATFORM: "binary_sensor", - CONF_DIM_STEPS: 15, - } - result3, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 2 - assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" - assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 - assert config_entry.options[CONF_X10][1][CONF_PLATFORM] == "binary_sensor" - assert config_entry.options[CONF_X10][1][CONF_DIM_STEPS] == 15 - - # If result2 eq result3 the changes will not save - assert result2["data"] != result3["data"] - - -async def test_options_remove_x10_device(hass: HomeAssistant) -> None: - """Test removing an X10 device.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_X10: [ - { - CONF_HOUSECODE: "C", - CONF_UNITCODE: 4, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 18, - }, - { - CONF_HOUSECODE: "D", - CONF_UNITCODE: 10, - CONF_PLATFORM: "binary_sensor", - CONF_DIM_STEPS: 15, - }, - ] - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_X10) - - user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 1 - - -async def test_options_remove_x10_device_with_override(hass: HomeAssistant) -> None: - """Test removing an X10 device when a device override is configured.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_X10: [ - { - CONF_HOUSECODE: "C", - CONF_UNITCODE: 4, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 18, - }, - { - CONF_HOUSECODE: "D", - CONF_UNITCODE: 10, - CONF_PLATFORM: "binary_sensor", - CONF_DIM_STEPS: 15, - }, - ], - CONF_OVERRIDE: [{CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 1, CONF_SUBCAT: 18}], - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_X10) - - user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 1 - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - - -async def test_options_override_bad_data(hass: HomeAssistant) -> None: - """Test for bad data in a device override.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) - - user_input = { - CONF_ADDRESS: "zzzzzz", - CONF_CAT: "bad", - CONF_SUBCAT: "data", - } - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "input_error"} - - async def test_discovery_via_usb(hass: HomeAssistant) -> None: """Test usb flow.""" discovery_info = usb.UsbServiceInfo( @@ -681,7 +281,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_usb" with patch(PATCH_CONNECTION), patch(PATCH_ASYNC_SETUP, return_value=True): @@ -690,7 +290,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {"device": "/dev/ttyINSTEON"} @@ -714,5 +314,5 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index a4e8da03345..c5524ff1919 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -1,6 +1,5 @@ """Test the init file for the Insteon component.""" -import asyncio from unittest.mock import patch import pytest @@ -11,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import MOCK_USER_INPUT_PLM, PATCH_CONNECTION +from .const import MOCK_USER_INPUT_PLM from .mock_devices import MockDevices from tests.common import MockConfigEntry @@ -70,22 +69,24 @@ async def test_setup_entry_failed_connection( async def test_import_frontend_dev_url(hass: HomeAssistant) -> None: """Test importing a dev_url config entry.""" - config = {} - config[DOMAIN] = {CONF_DEV_PATH: "/some/path"} + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_INPUT_PLM, options={CONF_DEV_PATH: "/some/path"} + ) + config_entry.add_to_hass(hass) with ( patch.object(insteon, "async_connect", new=mock_successful_connection), - patch.object(insteon, "close_insteon_connection"), + patch.object(insteon, "async_close") as mock_close, patch.object(insteon, "devices", new=MockDevices()), - patch( - PATCH_CONNECTION, - new=mock_successful_connection, - ), ): assert await async_setup_component( hass, insteon.DOMAIN, - config, + {}, ) await hass.async_block_till_done() - await asyncio.sleep(0.01) + assert hass.data[DOMAIN][CONF_DEV_PATH] == "/some/path" + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert insteon.devices.async_save.call_count == 1 + assert mock_close.called diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index 4f811e98de2..179984f20f2 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My integration" assert result["data"] == {} assert result["options"] == { @@ -96,7 +96,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -107,7 +107,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "round": 2.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "method": "left", "name": "My integration", diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 53763247bdf..555cb44caf5 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -629,8 +629,17 @@ async def test_device_class(hass: HomeAssistant, method) -> None: assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY -@pytest.mark.parametrize("method", ["trapezoidal", "left", "right"]) -async def test_calc_errors(hass: HomeAssistant, method) -> None: +@pytest.mark.parametrize( + ("method", "expected_states"), + [ + ("trapezoidal", [STATE_UNKNOWN, "0.500", "0.500"]), + ("left", [STATE_UNKNOWN, "0.000", "1.000"]), + ("right", ["0.000", "1.000", "1.000"]), + ], +) +async def test_calc_errors( + hass: HomeAssistant, method: str, expected_states: list[str] +) -> None: """Test integration sensor units using a power source.""" config = { "sensor": { @@ -649,9 +658,9 @@ async def test_calc_errors(hass: HomeAssistant, method) -> None: hass.states.async_set(entity_id, None, {}) await hass.async_block_till_done() - state = hass.states.get("sensor.integration") # With the source sensor in a None state, the Reimann sensor should be # unknown + state = hass.states.get("sensor.integration") assert state is not None assert state.state == STATE_UNKNOWN @@ -665,7 +674,7 @@ async def test_calc_errors(hass: HomeAssistant, method) -> None: state = hass.states.get("sensor.integration") assert state is not None - assert state.state == STATE_UNKNOWN if method != "right" else "0.000" + assert state.state == expected_states[0] # With the source sensor updated successfully, the Reimann sensor # should have a zero (known) value. @@ -677,7 +686,18 @@ async def test_calc_errors(hass: HomeAssistant, method) -> None: state = hass.states.get("sensor.integration") assert state is not None - assert round(float(state.state)) == 0 if method != "right" else 1 + assert state.state == expected_states[1] + + # Set the source sensor back to a non numeric state + now += timedelta(seconds=3600) + with freeze_time(now): + hass.states.async_set(entity_id, "unexpected", {"device_class": None}) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + assert state.state == expected_states[2] async def test_device_id( diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 7f6f509a3a3..ba4e2f039a3 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -36,7 +36,7 @@ async def test_no_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_device_entry" @@ -48,7 +48,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -57,7 +57,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Fireplace 12345" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -98,7 +98,7 @@ async def test_single_discovery( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "iftapi_connect"} @@ -131,7 +131,7 @@ async def test_single_discovery_loign_error( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "api_error"} @@ -195,14 +195,14 @@ async def test_multi_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_device" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -217,7 +217,7 @@ async def test_form_cannot_connect_manual_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( @@ -227,7 +227,7 @@ async def test_form_cannot_connect_manual_entry( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -262,7 +262,7 @@ async def test_picker_already_discovered( CONF_HOST: "192.168.1.4", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -299,7 +299,7 @@ async def test_reauth_flow( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -307,7 +307,7 @@ async def test_reauth_flow( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert entry.data[CONF_PASSWORD] == "AROONIE" assert entry.data[CONF_USERNAME] == "test" @@ -327,10 +327,10 @@ async def test_dhcp_discovery_intellifire_device( hostname="zentrios-Test", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "dhcp_confirm" result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "dhcp_confirm" result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={} @@ -358,5 +358,5 @@ async def test_dhcp_discovery_non_intellifire_device( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_intellifire_device" diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index d4980ba978e..a14d833c044 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -38,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", } @@ -50,7 +50,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +63,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "auth" with patch( @@ -79,7 +79,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "auth" assert result3["errors"] == {"base": "invalid_auth"} @@ -102,7 +102,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 assert result4["data"] == { "host": "1.1.1.1", @@ -126,7 +126,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -145,5 +145,5 @@ async def test_form_setup_exception(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/iotawatt/test_init.py b/tests/components/iotawatt/test_init.py index c185fec0e4d..8b707780eb4 100644 --- a/tests/components/iotawatt/test_init.py +++ b/tests/components/iotawatt/test_init.py @@ -24,7 +24,7 @@ async def test_setup_connection_failed( mock_iotawatt.connect.side_effect = httpx.ConnectError("") assert await async_setup_component(hass, "iotawatt", {}) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_auth_failed(hass: HomeAssistant, mock_iotawatt, entry) -> None: @@ -32,4 +32,4 @@ async def test_setup_auth_failed(hass: HomeAssistant, mock_iotawatt, entry) -> N mock_iotawatt.connect.return_value = False assert await async_setup_component(hass, "iotawatt", {}) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index 65cff43c8d4..799120e3966 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -1,8 +1,12 @@ """Tests for the IPMA component.""" -from collections import namedtuple from datetime import UTC, datetime +from pyipma.forecast import Forecast, Forecast_Location, Weather_Type +from pyipma.observation import Observation +from pyipma.rcm import RCM +from pyipma.uv import UV + from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME ENTRY_CONFIG = { @@ -18,109 +22,90 @@ class MockLocation: async def fire_risk(self, api): """Mock Fire Risk.""" - RCM = namedtuple( - "RCM", - [ - "dico", - "rcm", - "coordinates", - ], - ) return RCM("some place", 3, (0, 0)) async def uv_risk(self, api): """Mock UV Index.""" - UV = namedtuple( - "UV", - ["idPeriodo", "intervaloHora", "data", "globalIdLocal", "iUv"], - ) - return UV(0, "0", datetime.now(), 0, 5.7) + return UV(0, "0", datetime(2020, 1, 16, 0, 0, 0), 0, 5.7) async def observation(self, api): """Mock Observation.""" - Observation = namedtuple( - "Observation", - [ - "accumulated_precipitation", - "humidity", - "pressure", - "radiation", - "temperature", - "wind_direction", - "wind_intensity_km", - ], + return Observation( + precAcumulada=0.0, + humidade=71.0, + pressao=1000.0, + radiacao=0.0, + temperatura=18.0, + idDireccVento=8, + intensidadeVentoKM=3.94, + intensidadeVento=1.0944, + timestamp=datetime(2020, 1, 16, 0, 0, 0), + idEstacao=0, ) - return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94) - async def forecast(self, api, period): """Mock Forecast.""" - Forecast = namedtuple( - "Forecast", - [ - "feels_like_temperature", - "forecast_date", - "forecasted_hours", - "humidity", - "max_temperature", - "min_temperature", - "precipitation_probability", - "temperature", - "update_date", - "weather_type", - "wind_direction", - "wind_strength", - ], - ) - - WeatherType = namedtuple("WeatherType", ["id", "en", "pt"]) if period == 24: return [ Forecast( - None, - datetime(2020, 1, 16, 0, 0, 0), - 24, - None, - 16.2, - 10.6, - "100.0", - 13.4, - "2020-01-15T07:51:00", - WeatherType(9, "Rain/showers", "Chuva/aguaceiros"), - "S", - "10", + utci=None, + dataPrev=datetime(2020, 1, 16, 0, 0, 0), + idPeriodo=24, + hR=None, + tMax=16.2, + tMin=10.6, + probabilidadePrecipita=100.0, + tMed=13.4, + dataUpdate=datetime(2020, 1, 15, 7, 51, 0), + idTipoTempo=Weather_Type(9, "Rain/showers", "Chuva/aguaceiros"), + ddVento="S", + ffVento=10, + idFfxVento=0, + iUv=0, + intervaloHora="", + location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), ] if period == 1: return [ Forecast( - "7.7", - datetime(2020, 1, 15, 1, 0, 0, tzinfo=UTC), - 1, - "86.9", - 12.0, - None, - 80.0, - 10.6, - "2020-01-15T02:51:00", - WeatherType(10, "Light rain", "Chuva fraca ou chuvisco"), - "S", - "32.7", + utci=7.7, + dataPrev=datetime(2020, 1, 15, 1, 0, 0, tzinfo=UTC), + idPeriodo=1, + hR=86.9, + tMax=12.0, + tMin=None, + probabilidadePrecipita=80.0, + tMed=10.6, + dataUpdate=datetime(2020, 1, 15, 2, 51, 0), + idTipoTempo=Weather_Type( + 10, "Light rain", "Chuva fraca ou chuvisco" + ), + ddVento="S", + ffVento=32.7, + idFfxVento=0, + iUv=0, + intervaloHora="", + location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), Forecast( - "5.7", - datetime(2020, 1, 15, 2, 0, 0, tzinfo=UTC), - 1, - "86.9", - 12.0, - None, - 80.0, - 10.6, - "2020-01-15T02:51:00", - WeatherType(1, "Clear sky", "C\u00e9u limpo"), - "S", - "32.7", + utci=5.7, + dataPrev=datetime(2020, 1, 15, 2, 0, 0, tzinfo=UTC), + idPeriodo=1, + hR=86.9, + tMax=12.0, + tMin=None, + probabilidadePrecipita=80.0, + tMed=10.6, + dataUpdate=datetime(2020, 1, 15, 2, 51, 0), + idTipoTempo=Weather_Type(1, "Clear sky", "C\u00e9u limpo"), + ddVento="S", + ffVento=32.7, + idFfxVento=0, + iUv=0, + intervaloHora="", + location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), ] diff --git a/tests/components/ipma/snapshots/test_diagnostics.ambr b/tests/components/ipma/snapshots/test_diagnostics.ambr index c95364b6e4a..9d7d38db8c3 100644 --- a/tests/components/ipma/snapshots/test_diagnostics.ambr +++ b/tests/components/ipma/snapshots/test_diagnostics.ambr @@ -1,15 +1,10 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'current_weather': list([ - 0.0, - 71.0, - 1000.0, - 0.0, - 18.0, - 'NW', - 3.94, - ]), + 'current_weather': dict({ + '__type': "", + 'repr': 'Observation(intensidadeVentoKM=3.94, temperatura=18.0, radiacao=0.0, idDireccVento=8, precAcumulada=0.0, intensidadeVento=1.0944, humidade=71.0, pressao=1000.0, timestamp=datetime.datetime(2020, 1, 16, 0, 0), idEstacao=0)', + }), 'location_information': dict({ 'global_id_local': 1130600, 'id_station': 1200545, @@ -19,42 +14,14 @@ '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', - ]), + dict({ + '__type': "", + 'repr': "Forecast(tMed=10.6, tMin=None, ffVento=32.7, idFfxVento=0, dataUpdate=datetime.datetime(2020, 1, 15, 2, 51), tMax=12.0, iUv=0, intervaloHora='', idTipoTempo=Weather_Type(id=10, en='Light rain', pt='Chuva fraca ou chuvisco'), hR=86.9, location=Forecast_Location(globalIdLocal=0, local='', idRegiao=0, idDistrito=0, idConcelho=0, idAreaAviso='', coordinates=(0, 0)), probabilidadePrecipita=80.0, idPeriodo=1, dataPrev=datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), ddVento='S', utci=7.7)", + }), + dict({ + '__type': "", + 'repr': "Forecast(tMed=10.6, tMin=None, ffVento=32.7, idFfxVento=0, dataUpdate=datetime.datetime(2020, 1, 15, 2, 51), tMax=12.0, iUv=0, intervaloHora='', idTipoTempo=Weather_Type(id=1, en='Clear sky', pt='Céu limpo'), hR=86.9, location=Forecast_Location(globalIdLocal=0, local='', idRegiao=0, idDistrito=0, idConcelho=0, idAreaAviso='', coordinates=(0, 0)), probabilidadePrecipita=80.0, idPeriodo=1, dataPrev=datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), ddVento='S', utci=5.7)", + }), ]), }) # --- diff --git a/tests/components/ipma/snapshots/test_weather.ambr b/tests/components/ipma/snapshots/test_weather.ambr index 0a778776329..1142cb7cfe5 100644 --- a/tests/components/ipma/snapshots/test_weather.ambr +++ b/tests/components/ipma/snapshots/test_weather.ambr @@ -83,7 +83,7 @@ dict({ 'condition': 'rainy', 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', @@ -121,7 +121,7 @@ dict({ 'condition': 'rainy', 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', @@ -160,7 +160,7 @@ dict({ 'condition': 'rainy', 'datetime': '2020-01-16T00:00:00', - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', @@ -173,7 +173,7 @@ dict({ 'condition': 'rainy', 'datetime': '2020-01-16T00:00:00', - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index e17c8d011a9..ef9b667f03d 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -27,7 +27,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" test_data = { @@ -57,7 +57,7 @@ async def test_config_flow_failures(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" test_data = { @@ -109,5 +109,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant, init_integration) ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 1ea75ecf167..57e229a995b 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -40,7 +40,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_zeroconf_form( @@ -56,7 +56,7 @@ async def test_show_zeroconf_form( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} @@ -75,7 +75,7 @@ async def test_connection_error( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -93,7 +93,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -109,7 +109,7 @@ async def test_zeroconf_confirm_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -128,7 +128,7 @@ async def test_user_connection_upgrade_required( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -146,7 +146,7 @@ async def test_zeroconf_connection_upgrade_required( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_upgrade" @@ -164,7 +164,7 @@ async def test_user_parse_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -182,7 +182,7 @@ async def test_zeroconf_parse_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -200,7 +200,7 @@ async def test_user_ipp_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -218,7 +218,7 @@ async def test_zeroconf_ipp_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -236,7 +236,7 @@ async def test_user_ipp_version_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -254,7 +254,7 @@ async def test_zeroconf_ipp_version_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -273,7 +273,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -292,7 +292,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -316,7 +316,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -342,7 +342,7 @@ async def test_zeroconf_with_uuid_device_exists_abort_new_host( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "1.2.3.9" @@ -366,14 +366,14 @@ async def test_zeroconf_empty_unique_id( data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -399,14 +399,14 @@ async def test_zeroconf_no_unique_id( data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -428,14 +428,14 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.31" assert result["data"] @@ -459,13 +459,13 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -491,14 +491,14 @@ async def test_full_zeroconf_tls_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -535,14 +535,14 @@ async def test_zeroconf_empty_unique_id_uses_serial(hass: HomeAssistant) -> None data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index a75eed8ecd0..17c977a6b4c 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,9 +1,9 @@ """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 from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> None: @@ -11,7 +11,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -20,7 +20,7 @@ async def test_invalid_zip_code(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_ZIP_CODE: "bad"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_ZIP_CODE: "invalid_zip_code"} @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -38,6 +38,6 @@ async def test_step_user(hass: HomeAssistant, config, setup_iqvia) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 4df733a93fc..522006b0847 100644 --- a/tests/components/islamic_prayer_times/__init__.py +++ b/tests/components/islamic_prayer_times/__init__.py @@ -12,6 +12,17 @@ MOCK_USER_INPUT = { MOCK_CONFIG = {CONF_LATITUDE: 12.34, CONF_LONGITUDE: 23.45} + +PRAYER_TIMES_YESTERDAY = { + "Fajr": "2019-12-31T06:09:00+00:00", + "Sunrise": "2019-12-31T07:24:00+00:00", + "Dhuhr": "2019-12-31T12:29:00+00:00", + "Asr": "2019-12-31T15:31:00+00:00", + "Maghrib": "2019-12-31T17:34:00+00:00", + "Isha": "2019-12-31T18:52:00+00:00", + "Midnight": "2020-01-01T00:44:00+00:00", +} + PRAYER_TIMES = { "Fajr": "2020-01-01T06:10:00+00:00", "Sunrise": "2020-01-01T07:25:00+00:00", @@ -19,17 +30,17 @@ PRAYER_TIMES = { "Asr": "2020-01-01T15:32:00+00:00", "Maghrib": "2020-01-01T17:35:00+00:00", "Isha": "2020-01-01T18:53:00+00:00", - "Midnight": "2020-01-01T00:45:00+00:00", + "Midnight": "2020-01-02T00:45:00+00:00", } -NEW_PRAYER_TIMES = { - "Fajr": "2020-01-02T06:00:00+00:00", - "Sunrise": "2020-01-02T07:25:00+00:00", - "Dhuhr": "2020-01-02T12:30:00+00:00", - "Asr": "2020-01-02T15:32:00+00:00", - "Maghrib": "2020-01-02T17:45:00+00:00", - "Isha": "2020-01-02T18:53:00+00:00", - "Midnight": "2020-01-02T00:43:00+00:00", +PRAYER_TIMES_TOMORROW = { + "Fajr": "2020-01-02T06:11:00+00:00", + "Sunrise": "2020-01-02T07:26:00+00:00", + "Dhuhr": "2020-01-02T12:31:00+00:00", + "Asr": "2020-01-02T15:33:00+00:00", + "Maghrib": "2020-01-02T17:36:00+00:00", + "Isha": "2020-01-02T18:54:00+00:00", + "Midnight": "2020-01-03T00:46:00+00:00", } NOW = datetime(2020, 1, 1, 00, 00, 0, tzinfo=dt_util.UTC) diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 41a5c3df0ac..cb37a6b147d 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,12 +1,8 @@ """Tests for Islamic Prayer Times config flow.""" -from unittest.mock import patch - -from prayer_times_calculator import InvalidResponseError import pytest -from requests.exceptions import ConnectionError as ConnError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import ( CONF_CALC_METHOD, @@ -16,6 +12,7 @@ from homeassistant.components.islamic_prayer_times.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_CONFIG, MOCK_USER_INPUT @@ -29,50 +26,16 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.islamic_prayer_times.config_flow.async_validate_location", - return_value={}, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_INPUT - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "Home" - - -@pytest.mark.parametrize( - ("exception", "error"), - [ - (InvalidResponseError, "invalid_location"), - (ConnError, "conn_error"), - ], -) -async def test_flow_error( - hass: HomeAssistant, exception: Exception, error: str -) -> None: - """Test flow errors.""" - result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + await hass.async_block_till_done() - with patch( - "homeassistant.components.islamic_prayer_times.config_flow.PrayerTimesCalculator.fetch_prayer_times", - side_effect=exception, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_INPUT - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == error + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Home" async def test_options(hass: HomeAssistant) -> None: @@ -87,7 +50,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -99,7 +62,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_CALC_METHOD] == "makkah" assert result["data"][CONF_LAT_ADJ_METHOD] == "one_seventh" assert result["data"][CONF_MIDNIGHT_MODE] == "standard" @@ -115,7 +78,7 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -123,5 +86,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 3c7565a37ef..2a2597ef0ce 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -4,19 +4,18 @@ from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time -from prayer_times_calculator.exceptions import InvalidResponseError import pytest -from homeassistant import config_entries from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, STATE_UNAVAILABLE +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from . import NEW_PRAYER_TIMES, NOW, PRAYER_TIMES +from . import NOW, PRAYER_TIMES from tests.common import MockConfigEntry, async_fire_time_changed @@ -37,32 +36,13 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED - - -async def test_setup_failed(hass: HomeAssistant) -> None: - """Test Islamic Prayer Times failed due to an error.""" - - entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, - data={}, - ) - entry.add_to_hass(hass) - - # test request error raising ConfigEntryNotReady - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - side_effect=InvalidResponseError(), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.LOADED async def test_unload_entry(hass: HomeAssistant) -> None: @@ -74,14 +54,14 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_options_listener(hass: HomeAssistant) -> None: @@ -91,63 +71,23 @@ async def test_options_listener(hass: HomeAssistant) -> None: with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.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 + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 + mock_fetch_prayer_times.reset_mock() hass.config_entries.async_update_entry( entry, options={CONF_CALC_METHOD: "makkah"} ) await hass.async_block_till_done() - assert mock_fetch_prayer_times.call_count == 2 - - -async def test_update_failed(hass: HomeAssistant) -> None: - """Test integrations tries to update after 1 min if update fails.""" - 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), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state is config_entries.ConfigEntryState.LOADED - - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times" - ) as FetchPrayerTimes: - FetchPrayerTimes.side_effect = [ - InvalidResponseError, - NEW_PRAYER_TIMES, - ] - midnight_time = dt_util.parse_datetime(PRAYER_TIMES["Midnight"]) - assert midnight_time - future = midnight_time + timedelta(days=1, minutes=1) - with freeze_time(future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("sensor.islamic_prayer_times_fajr_prayer") - assert state.state == STATE_UNAVAILABLE - - # coordinator tries to update after 1 minute - future = future + timedelta(minutes=1) - with freeze_time(future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("sensor.islamic_prayer_times_fajr_prayer") - assert state.state == "2020-01-02T06:00:00+00:00" + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 @pytest.mark.parametrize( @@ -184,7 +124,7 @@ async def test_migrate_unique_id( with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), freeze_time(NOW), @@ -207,7 +147,7 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), freeze_time(NOW), @@ -220,3 +160,41 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: CONF_LONGITUDE: hass.config.longitude, } assert entry.minor_version == 2 + + +async def test_update_scheduling(hass: HomeAssistant) -> None: + """Test that integration schedules update immediately after Islamic midnight.""" + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry.add_to_hass(hass) + + with ( + patch( + "prayer_times_calculator_offline.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 entry.state is ConfigEntryState.LOADED + + with patch( + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ) as mock_fetch_prayer_times: + midnight_time = dt_util.parse_datetime(PRAYER_TIMES["Midnight"]) + assert midnight_time + with freeze_time(midnight_time): + async_fire_time_changed(hass, midnight_time) + await hass.async_block_till_done() + + mock_fetch_prayer_times.assert_not_called() + + midnight_time += timedelta(seconds=1) + with freeze_time(midnight_time): + async_fire_time_changed(hass, midnight_time) + await hass.async_block_till_done() + + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 22629819e05..7bd1a1192ad 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Islamic prayer times sensor platform.""" +from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time @@ -8,7 +9,7 @@ import pytest from homeassistant.components.islamic_prayer_times.const import DOMAIN from homeassistant.core import HomeAssistant -from . import NOW, PRAYER_TIMES +from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TOMORROW, PRAYER_TIMES_YESTERDAY from tests.common import MockConfigEntry @@ -31,20 +32,38 @@ def set_utc(hass: HomeAssistant) -> None: ("Midnight", "sensor.islamic_prayer_times_midnight_time"), ], ) +# In our example data, Islamic midnight occurs at 00:44 (yesterday's times, occurs today) and 00:45 (today's times, occurs tomorrow), +# hence we check that the times roll over at exactly the desired minute +@pytest.mark.parametrize( + ("offset", "prayer_times"), + [ + (timedelta(days=-1), PRAYER_TIMES_YESTERDAY), + (timedelta(minutes=44), PRAYER_TIMES_YESTERDAY), + (timedelta(minutes=44, seconds=1), PRAYER_TIMES), # Rolls over at 00:44 + 1 sec + (timedelta(days=1, minutes=45), PRAYER_TIMES), + ( + timedelta(days=1, minutes=45, seconds=1), # Rolls over at 00:45 + 1 sec + PRAYER_TIMES_TOMORROW, + ), + ], +) async def test_islamic_prayer_times_sensors( - hass: HomeAssistant, key: str, sensor_name: str + hass: HomeAssistant, + key: str, + sensor_name: str, + offset: timedelta, + prayer_times: dict[str, str], ) -> None: """Test minimum Islamic prayer times configuration.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) - with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", + side_effect=(PRAYER_TIMES_YESTERDAY, PRAYER_TIMES, PRAYER_TIMES_TOMORROW), ), - freeze_time(NOW), + freeze_time(NOW + offset), ): 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] + 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 77a61eaa770..2fa7b63e937 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -2,11 +2,11 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.iss.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch("homeassistant.components.iss.async_setup_entry", return_value=True): @@ -27,7 +27,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: {}, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -43,7 +43,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -69,5 +69,5 @@ async def test_options(hass: HomeAssistant) -> None: }, ) - assert configured.get("type") == "create_entry" + assert configured.get("type") is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: True} diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index b29b1dbc775..411439e2e70 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch from pyisy import ISYConnectionError, ISYInvalidAuthError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, ssdp from homeassistant.components.isy994.const import ( CONF_TLS_VER, @@ -17,6 +17,7 @@ from homeassistant.components.isy994.const import ( from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IGNORE, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -88,7 +89,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -103,7 +104,7 @@ async def test_form(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -126,7 +127,7 @@ async def test_form_invalid_host(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -144,7 +145,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -162,7 +163,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -180,7 +181,7 @@ async def test_form_isy_connection_error(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -200,7 +201,7 @@ async def test_form_isy_parse_response_error( MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert "ISY Could not parse response, poorly formatted XML." in caplog.text @@ -220,7 +221,7 @@ async def test_form_no_name_in_response(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -231,7 +232,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): @@ -239,7 +240,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant) -> None: result["flow_id"], MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: @@ -264,7 +265,7 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_ssdp(hass: HomeAssistant) -> None: @@ -283,7 +284,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -300,7 +301,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -333,7 +334,7 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80{ISY_URL_POSTFIX}" @@ -364,7 +365,7 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80/{ISY_URL_POSTFIX}" @@ -397,7 +398,7 @@ async def test_form_ssdp_existing_entry_with_alternate_port( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}" @@ -428,7 +429,7 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"https://3.3.3.3:443/{ISY_URL_POSTFIX}" @@ -445,7 +446,7 @@ async def test_form_dhcp(hass: HomeAssistant) -> None: macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -462,7 +463,7 @@ async def test_form_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -481,7 +482,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant) -> None: macaddress=MOCK_POLISY_MAC, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -502,7 +503,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_IOX_USER_INPUT @@ -521,7 +522,7 @@ async def test_form_dhcp_with_eisy(hass: HomeAssistant) -> None: macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -542,7 +543,7 @@ async def test_form_dhcp_with_eisy(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_IOX_USER_INPUT @@ -571,7 +572,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4{ISY_URL_POSTFIX}" @@ -601,7 +602,7 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" assert entry.data[CONF_USERNAME] == "bob" @@ -627,7 +628,7 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -648,7 +649,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "unique_id": MOCK_UUID}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -663,7 +664,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} with patch( @@ -678,7 +679,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} with ( @@ -698,5 +699,5 @@ async def test_reauth(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_setup_entry.called - assert result4["type"] == "abort" + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "reauth_successful" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 0988640d644..9f668e1ec62 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture @@ -46,10 +47,10 @@ async def test_not_found(hass: HomeAssistant, mock_disco) -> None: ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() @@ -79,10 +80,10 @@ async def test_found(hass: HomeAssistant, mock_disco) -> None: ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index 23c530d7e4d..b55766c2c68 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import MagicMock import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.jellyfin.const import CONF_CLIENT_DEVICE_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import async_load_json_fixture from .const import REAUTH_INPUT, TEST_PASSWORD, TEST_URL, TEST_USERNAME, USER_INPUT @@ -24,7 +25,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -40,7 +41,7 @@ async def test_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -49,7 +50,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "JELLYFIN-SERVER" assert result2["data"] == { CONF_CLIENT_DEVICE_ID: "TEST-UUID", @@ -74,7 +75,7 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client.auth.connect_to_address.return_value = await async_load_json_fixture( @@ -87,7 +88,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -103,7 +104,7 @@ async def test_form_invalid_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client.auth.login.return_value = await async_load_json_fixture( @@ -116,7 +117,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -130,7 +131,7 @@ async def test_form_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client.auth.connect_to_address.side_effect = Exception("UnknownException") @@ -141,7 +142,7 @@ async def test_form_exception( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -157,7 +158,7 @@ async def test_form_persists_device_id_on_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client_device_id.return_value = "TEST-UUID-1" @@ -171,7 +172,7 @@ async def test_form_persists_device_id_on_error( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} mock_client_device_id.return_value = "TEST-UUID-2" @@ -186,7 +187,7 @@ async def test_form_persists_device_id_on_error( await hass.async_block_till_done() assert result3 - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { CONF_CLIENT_DEVICE_ID: "TEST-UUID-1", CONF_URL: TEST_URL, @@ -225,7 +226,7 @@ async def test_reauth( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -241,7 +242,7 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -275,7 +276,7 @@ async def test_reauth_cannot_connect( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -290,7 +291,7 @@ async def test_reauth_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -308,7 +309,7 @@ async def test_reauth_cannot_connect( result["flow_id"], user_input=REAUTH_INPUT, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -342,7 +343,7 @@ async def test_reauth_invalid( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -353,7 +354,7 @@ async def test_reauth_invalid( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -369,7 +370,7 @@ async def test_reauth_invalid( result["flow_id"], user_input=REAUTH_INPUT, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -403,7 +404,7 @@ async def test_reauth_exception( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -416,7 +417,7 @@ async def test_reauth_exception( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -432,5 +433,5 @@ async def test_reauth_exception( result["flow_id"], user_input=REAUTH_INPUT, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py index 2a4be10b49b..48d63cd8cd0 100644 --- a/tests/components/juicenet/test_config_flow.py +++ b/tests/components/juicenet/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.juicenet.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def _mock_juicenet_return_value(get_devices=None): @@ -23,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "JuiceNet" assert result2["data"] == {CONF_ACCESS_TOKEN: "access_token"} assert len(mock_setup.mock_calls) == 1 @@ -64,7 +65,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -100,7 +101,7 @@ async def test_form_catch_unknown_errors(hass: HomeAssistant) -> None: result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -126,7 +127,7 @@ async def test_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "JuiceNet" assert result["data"] == {CONF_ACCESS_TOKEN: "access_token"} assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index b96b6c8aa5c..f66693a752c 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from justnimbus.exceptions import InvalidClientID, JustNimbusError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.justnimbus.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) @@ -62,7 +62,7 @@ async def test_form_errors( user_input=FIXTURE_USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == errors await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) @@ -81,7 +81,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") is None result2 = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: user_input=FIXTURE_USER_INPUT, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -108,7 +108,7 @@ async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "JustNimbus" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -134,7 +134,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_OLD_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -147,6 +147,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config.data == FIXTURE_USER_INPUT diff --git a/tests/components/jvc_projector/test_config_flow.py b/tests/components/jvc_projector/test_config_flow.py index a35dcd1ca38..282411540a4 100644 --- a/tests/components/jvc_projector/test_config_flow.py +++ b/tests/components/jvc_projector/test_config_flow.py @@ -26,7 +26,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -59,7 +59,7 @@ async def test_user_config_flow_bad_connect_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -73,7 +73,7 @@ async def test_user_config_flow_bad_connect_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -90,7 +90,7 @@ async def test_user_config_flow_device_exists_abort( context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -105,7 +105,7 @@ async def test_user_config_flow_bad_host_errors( data={CONF_HOST: "", CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -117,7 +117,7 @@ async def test_user_config_flow_bad_host_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -137,7 +137,7 @@ async def test_user_config_flow_bad_auth_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -151,7 +151,7 @@ async def test_user_config_flow_bad_auth_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -171,7 +171,7 @@ async def test_reauth_config_flow_success( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -179,7 +179,7 @@ async def test_reauth_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_integration.data[CONF_HOST] == MOCK_HOST @@ -202,7 +202,7 @@ async def test_reauth_config_flow_auth_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -210,7 +210,7 @@ async def test_reauth_config_flow_auth_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -226,7 +226,7 @@ async def test_reauth_config_flow_auth_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -234,7 +234,7 @@ async def test_reauth_config_flow_auth_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_integration.data[CONF_HOST] == MOCK_HOST @@ -257,7 +257,7 @@ async def test_reauth_config_flow_connect_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -265,7 +265,7 @@ async def test_reauth_config_flow_connect_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -281,7 +281,7 @@ async def test_reauth_config_flow_connect_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -289,7 +289,7 @@ async def test_reauth_config_flow_connect_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_integration.data[CONF_HOST] == MOCK_HOST diff --git a/tests/components/jvc_projector/test_select.py b/tests/components/jvc_projector/test_select.py new file mode 100644 index 00000000000..a52133bd688 --- /dev/null +++ b/tests/components/jvc_projector/test_select.py @@ -0,0 +1,44 @@ +"""Tests for JVC Projector select platform.""" + +from unittest.mock import MagicMock + +from jvcprojector import const + +from homeassistant.components.select import ( + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_OPTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +INPUT_ENTITY_ID = "select.jvc_projector_input" + + +async def test_input_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test input select.""" + entity = hass.states.get(INPUT_ENTITY_ID) + assert entity + assert entity.attributes.get(ATTR_FRIENDLY_NAME) == "JVC Projector Input" + assert entity.attributes.get(ATTR_OPTIONS) == [const.HDMI1, const.HDMI2] + assert entity.state == const.HDMI1 + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: INPUT_ENTITY_ID, + ATTR_OPTION: const.HDMI2, + }, + blocking=True, + ) + + mock_device.remote.assert_called_once_with(const.REMOTE_HDMI_2) diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py index 8171ed0955b..5d9f8dba146 100644 --- a/tests/components/kaleidescape/test_config_flow.py +++ b/tests/components/kaleidescape/test_config_flow.py @@ -21,7 +21,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -29,7 +29,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -44,7 +44,7 @@ async def test_user_config_flow_bad_connect_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -59,7 +59,7 @@ async def test_user_config_flow_unsupported_device_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unsupported"} @@ -71,7 +71,7 @@ async def test_user_config_flow_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_ssdp_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -91,7 +91,7 @@ async def test_ssdp_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -107,7 +107,7 @@ async def test_ssdp_config_flow_bad_connect_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -122,5 +122,5 @@ async def test_ssdp_config_flow_unsupported_device_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported" diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index e1cb083dc73..18bacc3a32c 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -7,11 +7,12 @@ from ndms2_client import ConnectionException from ndms2_client.client import InterfaceInfo, RouterInfo import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import keenetic_ndms2 as keenetic, ssdp from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS, MOCK_SSDP_DISCOVERY_INFO @@ -51,7 +52,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +64,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +99,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.options.async_configure( @@ -106,7 +107,7 @@ async def test_options(hass: HomeAssistant) -> None: user_input=MOCK_OPTIONS, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_OPTIONS @@ -126,7 +127,7 @@ async def test_host_already_configured(hass: HomeAssistant, connect) -> None: result["flow_id"], user_input=MOCK_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -139,7 +140,7 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -153,7 +154,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -168,7 +169,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +190,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,7 +211,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -236,7 +237,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == new_ip @@ -254,7 +255,7 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_udn" @@ -270,5 +271,5 @@ async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_keenetic_ndms2" diff --git a/tests/components/kegtron/test_config_flow.py b/tests/components/kegtron/test_config_flow.py index 4e21dc238bc..fe5cc0e5e5e 100644 --- a/tests/components/kegtron/test_config_flow.py +++ b/tests/components/kegtron/test_config_flow.py @@ -23,13 +23,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kegtron KT-100 9B75" assert result2["data"] == {} assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" @@ -42,7 +42,7 @@ async def test_async_step_bluetooth_not_kegtron(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_KEGTRON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -52,7 +52,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -66,14 +66,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "D0:CF:5E:5C:9B:75"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kegtron KT-200 9B75" assert result2["data"] == {} assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -103,7 +103,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "D0:CF:5E:5C:9B:75"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -125,7 +125,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -142,7 +142,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -153,7 +153,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -161,7 +161,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -174,7 +174,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -185,14 +185,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "D0:CF:5E:5C:9B:75"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kegtron KT-100 9B75" assert result2["data"] == {} assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" diff --git a/tests/components/keymitt_ble/test_config_flow.py b/tests/components/keymitt_ble/test_config_flow.py index 7e60bdfca53..029b50b9728 100644 --- a/tests/components/keymitt_ble/test_config_flow.py +++ b/tests/components/keymitt_ble/test_config_flow.py @@ -33,7 +33,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch_async_setup_entry() as mock_setup_entry, patch_microbot_api(): @@ -43,7 +43,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -64,7 +64,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +78,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -89,7 +89,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" assert result2["errors"] is None @@ -100,7 +100,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["result"].data == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_ACCESS_TOKEN: ANY, @@ -125,7 +125,7 @@ async def test_user_setup_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_user_no_devices(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -158,7 +158,7 @@ async def test_no_link(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -168,7 +168,7 @@ async def test_no_link(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( patch( @@ -183,7 +183,7 @@ async def test_no_link(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "link" assert result3["errors"] == {"base": "linking"} diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index 6b6ad4c1fcf..a200c25d2a3 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.kira as kira +from homeassistant.components import kira from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index 86c1698669e..e530ed0e6f3 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -30,7 +31,7 @@ async def test_import_once(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Kitchen Sink" assert result["data"] == {} assert result["options"] == {} @@ -45,7 +46,7 @@ async def test_import_once(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() @@ -63,5 +64,5 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/kitchen_sink/test_notify.py b/tests/components/kitchen_sink/test_notify.py new file mode 100644 index 00000000000..6d02bacb7be --- /dev/null +++ b/tests/components/kitchen_sink/test_notify.py @@ -0,0 +1,66 @@ +"""The tests for the demo button component.""" + +from collections.abc import AsyncGenerator +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.kitchen_sink import DOMAIN +from homeassistant.components.notify import ( + DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, +) +from homeassistant.components.notify.const import ATTR_MESSAGE +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +ENTITY_DIRECT_MESSAGE = "notify.mybox_personal_notifier" + + +@pytest.fixture +async def notify_only() -> AsyncGenerator[None, None]: + """Enable only the button platform.""" + with patch( + "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", + [Platform.NOTIFY], + ): + yield + + +@pytest.fixture(autouse=True) +async def setup_comp(hass: HomeAssistant, notify_only: None): + """Set up demo component.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_DIRECT_MESSAGE) + assert state + assert state.state == STATE_UNKNOWN + + +async def test_send_message( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test pressing the button.""" + state = hass.states.get(ENTITY_DIRECT_MESSAGE) + assert state + assert state.state == STATE_UNKNOWN + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + freezer.move_to(now) + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + {ATTR_ENTITY_ID: ENTITY_DIRECT_MESSAGE, ATTR_MESSAGE: "You have an update!"}, + blocking=True, + ) + + state = hass.states.get(ENTITY_DIRECT_MESSAGE) + assert state + assert state.state == now.isoformat() diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 222bb8bead2..21468584d81 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientConnectorError, ClientResponseError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -23,7 +24,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -74,19 +75,19 @@ async def test_form_options( assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_REVERSE: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_REVERSE: True} await hass.async_block_till_done() - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_form_invalid_auth(hass: HomeAssistant) -> None: @@ -108,7 +109,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -131,7 +132,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -154,5 +155,5 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index bd724029516..a580fc9eb2c 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -162,7 +162,7 @@ class KNXTestKit: if payload is not None: assert ( - telegram.payload.value.value == payload # type: ignore + telegram.payload.value.value == payload # type: ignore[attr-defined] ), f"Payload mismatch in {telegram} - Expected: {payload}" async def assert_read(self, group_address: str) -> None: @@ -279,4 +279,3 @@ def load_knxproj(hass_storage): "version": 1, "data": FIXTURE_PROJECT_DATA, } - return diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index f5b3d9595d0..f12a57f97ba 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -148,7 +148,7 @@ async def test_user_single_instance(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -163,7 +163,7 @@ async def test_routing_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_routing_setup( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -185,7 +185,7 @@ async def test_routing_setup( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -217,7 +217,7 @@ async def test_routing_setup_advanced( "show_advanced_options": True, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -226,7 +226,7 @@ async def test_routing_setup_advanced( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -240,7 +240,7 @@ async def test_routing_setup_advanced( CONF_KNX_LOCAL_IP: "no_local_ip", }, ) - assert result_invalid_input["type"] == FlowResultType.FORM + assert result_invalid_input["type"] is FlowResultType.FORM assert result_invalid_input["step_id"] == "routing" assert result_invalid_input["errors"] == { CONF_KNX_MCAST_GRP: "invalid_ip_address", @@ -260,7 +260,7 @@ async def test_routing_setup_advanced( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -288,7 +288,7 @@ async def test_routing_secure_manual_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -297,7 +297,7 @@ async def test_routing_secure_manual_setup( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -310,14 +310,14 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SECURE: True, }, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_routing" result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {"next_step_id": "secure_routing_manual"}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "secure_routing_manual" assert not result4["errors"] @@ -328,7 +328,7 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, }, ) - assert result_invalid_key1["type"] == FlowResultType.FORM + assert result_invalid_key1["type"] is FlowResultType.FORM assert result_invalid_key1["step_id"] == "secure_routing_manual" assert result_invalid_key1["errors"] == {"backbone_key": "invalid_backbone_key"} @@ -339,7 +339,7 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, }, ) - assert result_invalid_key2["type"] == FlowResultType.FORM + assert result_invalid_key2["type"] is FlowResultType.FORM assert result_invalid_key2["step_id"] == "secure_routing_manual" assert result_invalid_key2["errors"] == {"backbone_key": "invalid_backbone_key"} @@ -351,7 +351,7 @@ async def test_routing_secure_manual_setup( }, ) await hass.async_block_till_done() - assert secure_routing_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_routing_manual["type"] is FlowResultType.CREATE_ENTRY assert secure_routing_manual["title"] == "Secure Routing as 0.0.123" assert secure_routing_manual["data"] == { **DEFAULT_ENTRY_DATA, @@ -378,7 +378,7 @@ async def test_routing_secure_keyfile( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -387,7 +387,7 @@ async def test_routing_secure_keyfile( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -400,14 +400,14 @@ async def test_routing_secure_keyfile( CONF_KNX_ROUTING_SECURE: True, }, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_routing" result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "secure_knxkeys" assert not result4["errors"] @@ -420,7 +420,7 @@ async def test_routing_secure_keyfile( }, ) await hass.async_block_till_done() - assert routing_secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert routing_secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123" assert routing_secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, @@ -525,7 +525,7 @@ async def test_tunneling_setup_manual( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -534,7 +534,7 @@ async def test_tunneling_setup_manual( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert result2["errors"] == {"base": "no_tunnel_discovered"} @@ -553,7 +553,7 @@ async def test_tunneling_setup_manual( user_input, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == title assert result3["data"] == config_entry_data knx_setup.assert_called_once() @@ -682,7 +682,7 @@ async def test_tunneling_setup_manual_request_description_error( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Tunneling TCP @ 192.168.0.1" assert result["data"] == { **DEFAULT_ENTRY_DATA, @@ -716,7 +716,7 @@ async def test_tunneling_setup_for_local_ip( "show_advanced_options": True, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -725,7 +725,7 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert result2["errors"] == {"base": "no_tunnel_discovered"} @@ -739,7 +739,7 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) - assert result_invalid_host["type"] == FlowResultType.FORM + assert result_invalid_host["type"] is FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" assert result_invalid_host["errors"] == { CONF_HOST: "invalid_ip_address", @@ -755,7 +755,7 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_LOCAL_IP: "asdf", }, ) - assert result_invalid_local["type"] == FlowResultType.FORM + assert result_invalid_local["type"] is FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" assert result_invalid_local["errors"] == { CONF_KNX_LOCAL_IP: "invalid_ip_address", @@ -773,7 +773,7 @@ async def test_tunneling_setup_for_local_ip( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling UDP @ 192.168.0.2" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -804,7 +804,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -813,7 +813,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert tunnel_flow["type"] == FlowResultType.FORM + assert tunnel_flow["type"] is FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -822,7 +822,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -859,7 +859,7 @@ async def test_manual_tunnel_step_with_found_gateway( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -868,7 +868,7 @@ async def test_manual_tunnel_step_with_found_gateway( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert tunnel_flow["type"] == FlowResultType.FORM + assert tunnel_flow["type"] is FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -878,7 +878,7 @@ async def test_manual_tunnel_step_with_found_gateway( CONF_KNX_GATEWAY: OPTION_MANUAL_TUNNEL, }, ) - assert manual_tunnel_flow["type"] == FlowResultType.FORM + assert manual_tunnel_flow["type"] is FlowResultType.FORM assert manual_tunnel_flow["step_id"] == "manual_tunnel" assert not manual_tunnel_flow["errors"] @@ -896,7 +896,7 @@ async def test_form_with_automatic_connection_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -906,7 +906,7 @@ async def test_form_with_automatic_connection_handling( }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { # don't use **DEFAULT_ENTRY_DATA here to check for correct usage of defaults @@ -939,7 +939,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -948,7 +948,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" assert not result2["errors"] @@ -956,7 +956,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: result2["flow_id"], {CONF_KNX_GATEWAY: str(gateway)}, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_tunnel" return result3 @@ -988,7 +988,7 @@ async def test_get_secure_menu_step_manual_tunnelling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -997,7 +997,7 @@ async def test_get_secure_menu_step_manual_tunnelling( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" assert not result2["errors"] @@ -1016,7 +1016,7 @@ async def test_get_secure_menu_step_manual_tunnelling( CONF_PORT: 3675, }, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_tunnel" @@ -1028,7 +1028,7 @@ async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) -> menu_step["flow_id"], {"next_step_id": "secure_tunnel_manual"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_tunnel_manual" assert not result["errors"] @@ -1041,7 +1041,7 @@ async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) -> }, ) await hass.async_block_till_done() - assert secure_tunnel_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_tunnel_manual["type"] is FlowResultType.CREATE_ENTRY assert secure_tunnel_manual["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -1066,7 +1066,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -1078,7 +1078,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: CONF_KNX_KNXKEY_PASSWORD: "test", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert secure_knxkeys["step_id"] == "knxkeys_tunnel_select" assert not result["errors"] secure_knxkeys = await hass.config_entries.flow.async_configure( @@ -1087,7 +1087,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -1116,7 +1116,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant) - menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -1130,7 +1130,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant) - CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - assert secure_knxkeys["type"] == FlowResultType.FORM + assert secure_knxkeys["type"] is FlowResultType.FORM assert secure_knxkeys["errors"] assert ( secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] @@ -1146,7 +1146,7 @@ async def test_configure_secure_knxkeys_no_tunnel_for_host(hass: HomeAssistant) menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -1159,7 +1159,7 @@ async def test_configure_secure_knxkeys_no_tunnel_for_host(hass: HomeAssistant) CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - assert secure_knxkeys["type"] == FlowResultType.FORM + assert secure_knxkeys["type"] is FlowResultType.FORM assert secure_knxkeys["errors"] == {"base": "keyfile_no_tunnel_for_host"} @@ -1183,7 +1183,7 @@ async def test_options_flow_connection_type( menu_step["flow_id"], {"next_step_id": "connection_type"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connection_type" result2 = await hass.config_entries.options.async_configure( @@ -1192,7 +1192,7 @@ async def test_options_flow_connection_type( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" result3 = await hass.config_entries.options.async_configure( @@ -1202,7 +1202,7 @@ async def test_options_flow_connection_type( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert not result3["data"] assert mock_config_entry.data == { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -1263,7 +1263,7 @@ async def test_options_flow_secure_manual_to_keyfile( menu_step["flow_id"], {"next_step_id": "connection_type"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connection_type" result2 = await hass.config_entries.options.async_configure( @@ -1272,7 +1272,7 @@ async def test_options_flow_secure_manual_to_keyfile( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" assert not result2["errors"] @@ -1280,14 +1280,14 @@ async def test_options_flow_secure_manual_to_keyfile( result2["flow_id"], {CONF_KNX_GATEWAY: str(gateway)}, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_tunnel" result4 = await hass.config_entries.options.async_configure( result3["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "secure_knxkeys" assert not result4["errors"] @@ -1299,7 +1299,7 @@ async def test_options_flow_secure_manual_to_keyfile( CONF_KNX_KNXKEY_PASSWORD: "test", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert secure_knxkeys["step_id"] == "knxkeys_tunnel_select" assert not result["errors"] secure_knxkeys = await hass.config_entries.options.async_configure( @@ -1308,7 +1308,7 @@ async def test_options_flow_secure_manual_to_keyfile( ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.data == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -1341,7 +1341,7 @@ async def test_options_communication_settings( menu_step["flow_id"], {"next_step_id": "communication_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "communication_settings" result2 = await hass.config_entries.options.async_configure( @@ -1353,7 +1353,7 @@ async def test_options_communication_settings( }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { **DEFAULT_ENTRY_DATA, @@ -1394,7 +1394,7 @@ async def test_options_update_keyfile(hass: HomeAssistant, knx_setup) -> None: menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" with patch_file_upload(): @@ -1406,7 +1406,7 @@ async def test_options_update_keyfile(hass: HomeAssistant, knx_setup) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { **start_data, @@ -1442,7 +1442,7 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" with patch_file_upload(): @@ -1454,7 +1454,7 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "knxkeys_tunnel_select" result3 = await hass.config_entries.options.async_configure( @@ -1464,7 +1464,7 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert not result3.get("data") assert mock_config_entry.data == { **start_data, diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 2d2889e7718..a317a6a298c 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -11,7 +11,6 @@ from xknx.io import ( SecureConfig, ) -from homeassistant import config_entries from homeassistant.components.knx.config_flow import DEFAULT_ROUTING_IA from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, @@ -40,6 +39,7 @@ from homeassistant.components.knx.const import ( DOMAIN as KNX_DOMAIN, KNXConfigEntryData, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant @@ -287,4 +287,4 @@ async def test_async_remove_entry( await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/knx/test_notify.py b/tests/components/knx/test_notify.py index d843c460c34..94f2d579fc8 100644 --- a/tests/components/knx/test_notify.py +++ b/tests/components/knx/test_notify.py @@ -1,5 +1,6 @@ """Test KNX notify.""" +from homeassistant.components import notify from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.schema import NotifySchema from homeassistant.const import CONF_NAME, CONF_TYPE @@ -8,7 +9,9 @@ from homeassistant.core import HomeAssistant from .conftest import KNXTestKit -async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_legacy_notify_service_simple( + hass: HomeAssistant, knx: KNXTestKit +) -> None: """Test KNX notify can send to one device.""" await knx.setup_integration( { @@ -26,22 +29,7 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_write( "1/0/0", - ( - 0x49, - 0x20, - 0x6C, - 0x6F, - 0x76, - 0x65, - 0x20, - 0x4B, - 0x4E, - 0x58, - 0x0, - 0x0, - 0x0, - 0x0, - ), + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0), ) await hass.services.async_call( @@ -56,26 +44,11 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_write( "1/0/0", - ( - 0x49, - 0x20, - 0x6C, - 0x6F, - 0x76, - 0x65, - 0x20, - 0x4B, - 0x4E, - 0x58, - 0x2C, - 0x20, - 0x62, - 0x75, - ), + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117), ) -async def test_notify_multiple_sends_to_all_with_different_encodings( +async def test_legacy_notify_service_multiple_sends_to_all_with_different_encodings( hass: HomeAssistant, knx: KNXTestKit ) -> None: """Test KNX notify `type` configuration.""" @@ -110,3 +83,91 @@ async def test_notify_multiple_sends_to_all_with_different_encodings( "1/0/1", (71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0), ) + + +async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX notify can send to one device.""" + await knx.setup_integration( + { + NotifySchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: "1/0/0", + } + } + ) + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.test", + notify.ATTR_MESSAGE: "I love KNX", + }, + ) + await knx.assert_write( + "1/0/0", + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0), + ) + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.test", + notify.ATTR_MESSAGE: "I love KNX, but this text is too long for KNX, poor KNX", + }, + ) + await knx.assert_write( + "1/0/0", + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117), + ) + + +async def test_notify_multiple_sends_with_different_encodings( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX notify `type` configuration.""" + await knx.setup_integration( + { + NotifySchema.PLATFORM: [ + { + CONF_NAME: "ASCII", + KNX_ADDRESS: "1/0/0", + CONF_TYPE: "string", + }, + { + CONF_NAME: "Latin-1", + KNX_ADDRESS: "1/0/1", + CONF_TYPE: "latin_1", + }, + ] + } + ) + message = {notify.ATTR_MESSAGE: "Gänsefüßchen"} + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.ascii", + **message, + }, + ) + await knx.assert_write( + "1/0/0", + # "G?nsef??chen" + (71, 63, 110, 115, 101, 102, 63, 63, 99, 104, 101, 110, 0, 0), + ) + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.latin_1", + **message, + }, + ) + await knx.assert_write( + "1/0/1", + (71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0), + ) diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index be3fe070c10..5eec4530d4e 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -6,6 +6,7 @@ from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import NumberSchema from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from .conftest import KNXTestKit @@ -37,14 +38,14 @@ async def test_number_set_value(hass: HomeAssistant, knx: KNXTestKit) -> None: assert state.attributes.get("unit_of_measurement") == "%" # set value out of range - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( "number", "set_value", {"entity_id": "number.test", "value": 101.0}, blocking=True, ) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( "number", "set_value", diff --git a/tests/components/knx/test_repairs.py b/tests/components/knx/test_repairs.py new file mode 100644 index 00000000000..4ad06e0addb --- /dev/null +++ b/tests/components/knx/test_repairs.py @@ -0,0 +1,84 @@ +"""Test repairs for KNX integration.""" + +from http import HTTPStatus + +from homeassistant.components.knx.const import DOMAIN, KNX_ADDRESS +from homeassistant.components.knx.schema import NotifySchema +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.issue_registry as ir + +from .conftest import KNXTestKit + +from tests.typing import ClientSessionGenerator + + +async def test_knx_notify_service_issue( + hass: HomeAssistant, + knx: KNXTestKit, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the legacy notify service still works before migration and repair flow is triggered.""" + await knx.setup_integration( + { + NotifySchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: "1/0/0", + } + } + ) + http_client = await hass_client() + + # Assert no issue is present + assert len(issue_registry.issues) == 0 + + # Simulate legacy service being used + assert hass.services.has_service(NOTIFY_DOMAIN, NOTIFY_DOMAIN) + await hass.services.async_call( + NOTIFY_DOMAIN, + NOTIFY_DOMAIN, + service_data={"message": "It is too cold!", "target": "test"}, + blocking=True, + ) + await knx.assert_write( + "1/0/0", + (73, 116, 32, 105, 115, 32, 116, 111, 111, 32, 99, 111, 108, 100), + ) + + # Assert the issue is present + assert len(issue_registry.issues) == 1 + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + + # Test confirm step in repair flow + resp = await http_client.post( + RepairsFlowIndexView.url, + json={"handler": DOMAIN, "issue_id": "migrate_notify"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + resp = await http_client.post( + RepairsFlowResourceView.url.format(flow_id=flow_id), + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["type"] == "create_entry" + + # Assert the issue is no longer present + assert not issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + assert len(issue_registry.issues) == 0 diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index ecc3bc1f672..d570654be93 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.kodi.config_flow import ( ) from homeassistant.components.kodi.const import DEFAULT_TIMEOUT, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import ( TEST_CREDENTIALS, @@ -34,7 +35,7 @@ async def user_flow(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} return result["flow_id"] @@ -59,7 +60,7 @@ async def test_user_flow(hass: HomeAssistant, user_flow) -> None: result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -87,7 +88,7 @@ async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} @@ -110,7 +111,7 @@ async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -142,7 +143,7 @@ async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -165,7 +166,7 @@ async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -198,7 +199,7 @@ async def test_form_empty_ws_port(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -211,7 +212,7 @@ async def test_form_empty_ws_port(hass: HomeAssistant, user_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -239,7 +240,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} @@ -257,7 +258,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {"base": "invalid_auth"} @@ -275,7 +276,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -293,7 +294,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {"base": "unknown"} @@ -316,7 +317,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -335,7 +336,7 @@ async def test_form_cannot_connect_http(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -354,7 +355,7 @@ async def test_form_exception_http(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -378,7 +379,7 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -399,7 +400,7 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_WS_PORT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "cannot_connect"} @@ -417,7 +418,7 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_WS_PORT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "cannot_connect"} @@ -441,7 +442,7 @@ async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -460,7 +461,7 @@ async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_WS_PORT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "unknown"} @@ -483,7 +484,7 @@ async def test_discovery(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with patch( @@ -495,7 +496,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "hostname" assert result["data"] == { **TEST_HOST, @@ -527,7 +528,7 @@ async def test_discovery_cannot_connect_http(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -554,7 +555,7 @@ async def test_discovery_cannot_connect_ws(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -577,7 +578,7 @@ async def test_discovery_exception_http(hass: HomeAssistant, user_flow) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -599,7 +600,7 @@ async def test_discovery_invalid_auth(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} @@ -622,14 +623,14 @@ async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -647,7 +648,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" @@ -663,7 +664,7 @@ async def test_discovery_without_unique_id(hass: HomeAssistant) -> None: data=TEST_DISCOVERY_WO_UUID, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_uuid" @@ -690,7 +691,7 @@ async def test_form_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_IMPORT["name"] assert result["data"] == TEST_IMPORT @@ -715,7 +716,7 @@ async def test_form_import_invalid_auth(hass: HomeAssistant) -> None: data=TEST_IMPORT, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -737,7 +738,7 @@ async def test_form_import_cannot_connect(hass: HomeAssistant) -> None: data=TEST_IMPORT, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -759,5 +760,5 @@ async def test_form_import_exception(hass: HomeAssistant) -> None: data=TEST_IMPORT, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 1737fe5d7c9..2a3c1f7544f 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.kodi import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index dba0822b1d8..6217a77903b 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -57,8 +57,7 @@ def get_kodi_connection( """Get Kodi connection.""" if ws_port is None: return MockConnection() - else: - return MockWSConnection() + return MockWSConnection() class MockConnection: diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index c46e115d159..5865616c544 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components import konnected, ssdp from homeassistant.components.konnected import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +34,7 @@ async def test_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_panel.get_status.return_value = { @@ -43,7 +44,7 @@ async def test_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel", @@ -55,7 +56,7 @@ async def test_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["host"] == "1.2.3.4" assert result["data"]["port"] == 1234 assert result["data"]["model"] == "Konnected" @@ -70,7 +71,7 @@ async def test_pro_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # pro uses chipId instead of MAC as unique id @@ -82,7 +83,7 @@ async def test_pro_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel Pro", @@ -94,7 +95,7 @@ async def test_pro_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["host"] == "1.2.3.4" assert result["data"]["port"] == 1234 assert result["data"]["model"] == "Konnected Pro" @@ -126,7 +127,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel", @@ -151,7 +152,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # Test abort if invalid data @@ -167,7 +168,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" # Test abort if invalid manufacturer @@ -185,7 +186,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_konn_panel" # Test abort if invalid model @@ -203,7 +204,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_konn_panel" # Test abort if already configured @@ -227,7 +228,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -267,21 +268,21 @@ async def test_import_no_host_user_finish(hass: HomeAssistant, mock_panel) -> No "id": "aabbccddeeff", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "import_confirm" assert result["description_placeholders"]["id"] == "aabbccddeeff" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # confirm user is prompted to enter host result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"host": "1.1.1.1", "port": 1234} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel Pro", @@ -294,7 +295,7 @@ async def test_import_no_host_user_finish(hass: HomeAssistant, mock_panel) -> No result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> None: @@ -334,7 +335,7 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> "id": "somechipid", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "import_confirm" assert result["description_placeholders"]["id"] == "somechipid" @@ -352,13 +353,13 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> }, ), ) - assert ssdp_result["type"] == "abort" + assert ssdp_result["type"] is FlowResultType.ABORT assert ssdp_result["reason"] == "already_in_progress" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel Pro", @@ -371,7 +372,7 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_ssdp_already_configured(hass: HomeAssistant, mock_panel) -> None: @@ -399,7 +400,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant, mock_panel) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -479,7 +480,7 @@ async def test_ssdp_host_update(hass: HomeAssistant, mock_panel) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT # confirm the host value was updated, access_token was not entry = hass.config_entries.async_entries(config_flow.DOMAIN)[0] @@ -537,13 +538,13 @@ async def test_import_existing_config(hass: HomeAssistant, mock_panel) -> None: } ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "host": "1.2.3.4", "port": 1234, @@ -665,7 +666,7 @@ async def test_import_existing_config_entry(hass: HomeAssistant, mock_panel) -> }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT # We should have updated the host info but not the access token assert len(hass.config_entries.async_entries("konnected")) == 1 @@ -717,13 +718,13 @@ async def test_import_pin_config(hass: HomeAssistant, mock_panel) -> None: } ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "host": "1.2.3.4", "port": 1234, @@ -802,7 +803,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" result = await hass.config_entries.options.async_configure( @@ -817,7 +818,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: "out": "Switchable Output", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" assert result["description_placeholders"] == { "zone": "Zone 2", @@ -827,7 +828,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" assert result["description_placeholders"] == { "zone": "Zone 6", @@ -838,7 +839,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={"type": "window", "name": "winder", "inverse": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" assert result["description_placeholders"] == { "zone": "Zone 3", @@ -848,7 +849,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "dht"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "Zone 4", @@ -859,7 +860,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "OUT", @@ -879,7 +880,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "OUT", @@ -899,7 +900,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" # make sure we enforce url format result = await hass.config_entries.options.async_configure( @@ -912,7 +913,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -923,7 +924,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: "api_host": "http://overridehost:1111", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "io": { "2": "Binary Sensor", @@ -988,7 +989,7 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" result = await hass.config_entries.options.async_configure( @@ -1003,7 +1004,7 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: "7": "Digital Sensor", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io_ext" result = await hass.config_entries.options.async_configure( @@ -1019,14 +1020,14 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: "alarm2_out2": "Disabled", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 2 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 6 @@ -1034,42 +1035,42 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={"type": "window", "name": "winder", "inverse": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 10 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 11 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "window"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" # zone 3 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "dht"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" # zone 7 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "ds18b20", "name": "temper"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone 4 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone 8 @@ -1083,21 +1084,21 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: "repeat": 4, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone out1 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone alarm1 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" result = await hass.config_entries.options.async_configure( @@ -1105,7 +1106,7 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: user_input={"discovery": False, "blink": True, "override_api_host": False}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "io": { "10": "Binary Sensor", @@ -1201,7 +1202,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" # confirm the defaults are set based on current config - we"ll spot check this throughout @@ -1218,7 +1219,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: "3": "Switchable Output", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io_ext" schema = result["data_schema"]({}) assert schema["8"] == "Disabled" @@ -1227,7 +1228,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 1 @@ -1238,7 +1239,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" # zone 2 @@ -1249,7 +1250,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={"type": "dht"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone 3 @@ -1263,7 +1264,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"activation": "high", "more_states": "No"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" schema = result["data_schema"]({}) @@ -1275,7 +1276,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: ) # verify the updated fields - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "io": {"1": "Binary Sensor", "2": "Digital Sensor", "3": "Switchable Output"}, "discovery": True, @@ -1348,7 +1349,7 @@ async def test_option_flow_existing(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" # confirm the defaults are pulled in from the existing options diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 41acfb1d136..d94256ebf1a 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_form_g1( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -91,7 +92,7 @@ async def test_form_g1( "scb:network", "Hostname" ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "scb" assert result2["data"] == { "host": "1.1.1.1", @@ -110,7 +111,7 @@ async def test_form_g2( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -157,7 +158,7 @@ async def test_form_g2( "scb:network", "Network:Hostname" ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "scb" assert result2["data"] == { "host": "1.1.1.1", @@ -196,7 +197,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -230,7 +231,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"host": "cannot_connect"} @@ -264,7 +265,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -288,5 +289,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py index e6639264291..e1971ec3ab8 100644 --- a/tests/components/kraken/test_config_flow.py +++ b/tests/components/kraken/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components.kraken.const import CONF_TRACKED_ASSET_PAIRS, DOMAIN from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE @@ -20,13 +21,13 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -37,7 +38,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -86,7 +87,7 @@ async def test_options(hass: HomeAssistant) -> None: CONF_TRACKED_ASSET_PAIRS: ["ADA/ETH"], }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index 9638df360c8..a2f3949bd07 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -7,6 +7,7 @@ import pykulersky from homeassistant import config_entries from homeassistant.components.kulersky.config_flow import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_success(hass: HomeAssistant) -> None: @@ -15,7 +16,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None light = MagicMock(spec=pykulersky.Light) @@ -37,7 +38,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kuler Sky" assert result2["data"] == {} @@ -50,7 +51,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -69,7 +70,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -80,7 +81,7 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -99,6 +100,6 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 195c004179b..5a48b3d15fe 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "location" assert result2["errors"] is None @@ -54,7 +54,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Test" assert result3["data"] == { "username": "test-username", @@ -83,7 +83,7 @@ async def test_form_auth_false(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -102,7 +102,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -124,7 +124,7 @@ async def test_form_login_first(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -149,7 +149,7 @@ async def test_form_no_locations(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_locations"} @@ -171,7 +171,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -196,7 +196,7 @@ async def test_already_configured_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -218,7 +218,7 @@ async def test_already_configured_device( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "location" assert result2["errors"] is None @@ -230,7 +230,7 @@ async def test_already_configured_device( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -261,7 +261,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" new_username = "new-username" @@ -283,7 +283,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index cf11e787ad8..51fa7e5abf4 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -35,11 +35,11 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED async def test_login_error(hass: HomeAssistant) -> None: @@ -54,7 +54,7 @@ async def test_login_error(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows assert len(flows) == 1 @@ -76,7 +76,7 @@ async def test_http_error(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: @@ -98,7 +98,7 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, @@ -135,7 +135,7 @@ async def test_failed_token( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError("Test")): freezer.tick(timedelta(hours=1)) @@ -145,7 +145,7 @@ async def test_failed_token( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index b9140e6173f..11faaf8877e 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -43,7 +43,7 @@ async def test_entities_added(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature") @@ -67,7 +67,7 @@ async def test_sensor_permission( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR assert not hass.states.get("sensor.test_temperature") assert "This account does not have permission to read Test" in caplog.text @@ -92,7 +92,7 @@ async def test_field_not_supported( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_some_unsupported_field") is None assert "Unsupported sensor field" in caplog.text @@ -128,7 +128,7 @@ async def test_field_types( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get(f"sensor.test_{entity_id}").state == expected @@ -151,7 +151,7 @@ async def test_no_field(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature").state == "unavailable" @@ -174,5 +174,5 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature").state == "unknown" diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index 37d9c9a3e95..14f794000d8 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.lamarzocco.const import ( CONF_USE_BLUETOOTH, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState 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 @@ -30,7 +30,7 @@ async def __do_successful_user_step( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" return result2 @@ -48,7 +48,7 @@ async def __do_sucessful_machine_selection_step( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == mock_lamarzocco.serial_number assert result3["data"] == { @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant, mock_lamarzocco: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -82,7 +82,7 @@ async def test_form_abort_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -91,7 +91,7 @@ async def test_form_abort_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" result3 = await hass.config_entries.flow.async_configure( @@ -103,7 +103,7 @@ async def test_form_abort_already_configured( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_form_invalid_auth( USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -139,7 +139,7 @@ async def test_form_invalid_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -150,7 +150,7 @@ async def test_form_invalid_host( mock_lamarzocco.check_local_connection.return_value = False - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" result3 = await hass.config_entries.flow.async_configure( @@ -162,7 +162,7 @@ async def test_form_invalid_host( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"host": "cannot_connect"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -187,7 +187,7 @@ async def test_form_cannot_connect( USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_machines"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -197,7 +197,7 @@ async def test_form_cannot_connect( USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 @@ -224,7 +224,7 @@ async def test_reauth_flow( data=mock_config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_reauth_flow( {CONF_PASSWORD: "new_password"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert result2["reason"] == "reauth_successful" assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -250,14 +250,14 @@ async def test_bluetooth_discovery( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=service_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -269,7 +269,7 @@ async def test_bluetooth_discovery( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == mock_lamarzocco.serial_number assert result3["data"] == { @@ -296,7 +296,7 @@ async def test_bluetooth_discovery_errors( data=service_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_lamarzocco.get_all_machines.return_value = [("GS98765", "GS3 MP")] @@ -304,7 +304,7 @@ async def test_bluetooth_discovery_errors( result["flow_id"], USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "machine_not_found"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -315,7 +315,7 @@ async def test_bluetooth_discovery_errors( result["flow_id"], USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 @@ -327,7 +327,7 @@ async def test_bluetooth_discovery_errors( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == mock_lamarzocco.serial_number assert result3["data"] == { @@ -346,11 +346,11 @@ async def test_options_flow( ) -> None: """Test options flow.""" await async_init_integration(hass, mock_config_entry) - assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -361,7 +361,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_USE_BLUETOOTH: False, } diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index e5fa1229e07..2a21423ad03 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -60,7 +60,7 @@ async def test_full_cloud_import_flow_multiple_devices( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -77,7 +77,7 @@ async def test_full_cloud_import_flow_multiple_devices( }, ) - assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("type") is FlowResultType.EXTERNAL_STEP assert result2.get("url") == ( "https://developer.lametric.com/api/v2/oauth2/authorize" "?response_type=code&client_id=client" @@ -103,14 +103,14 @@ async def test_full_cloud_import_flow_multiple_devices( result3 = await hass.config_entries.flow.async_configure(flow_id) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "cloud_select_device" result4 = await hass.config_entries.flow.async_configure( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "Frenck's LaMetric" assert result4.get("data") == { CONF_HOST: "127.0.0.1", @@ -140,7 +140,7 @@ async def test_full_cloud_import_flow_single_device( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -157,7 +157,7 @@ async def test_full_cloud_import_flow_single_device( }, ) - assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("type") is FlowResultType.EXTERNAL_STEP assert result2.get("url") == ( "https://developer.lametric.com/api/v2/oauth2/authorize" "?response_type=code&client_id=client" @@ -188,7 +188,7 @@ async def test_full_cloud_import_flow_single_device( ] result3 = await hass.config_entries.flow.async_configure(flow_id) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -214,7 +214,7 @@ async def test_full_manual( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -223,14 +223,14 @@ async def test_full_manual( flow_id, user_input={"next_step_id": "manual_entry"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "manual_entry" result3 = await hass.config_entries.flow.async_configure( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -263,7 +263,7 @@ async def test_full_ssdp_with_cloud_import( DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -280,7 +280,7 @@ async def test_full_ssdp_with_cloud_import( }, ) - assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("type") is FlowResultType.EXTERNAL_STEP assert result2.get("url") == ( "https://developer.lametric.com/api/v2/oauth2/authorize" "?response_type=code&client_id=client" @@ -306,7 +306,7 @@ async def test_full_ssdp_with_cloud_import( result3 = await hass.config_entries.flow.async_configure(flow_id) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -332,7 +332,7 @@ async def test_full_ssdp_manual_entry( DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -341,14 +341,14 @@ async def test_full_ssdp_manual_entry( flow_id, user_input={"next_step_id": "manual_entry"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "manual_entry" result3 = await hass.config_entries.flow.async_configure( flow_id, user_input={CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -390,7 +390,7 @@ async def test_ssdp_abort_invalid_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=data ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == reason @@ -439,7 +439,7 @@ async def test_cloud_import_updates_existing_entry( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -473,7 +473,7 @@ async def test_manual_updates_existing_entry( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -495,7 +495,7 @@ async def test_discovery_updates_existing_entry( DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -544,7 +544,7 @@ async def test_cloud_abort_no_devices( mock_lametric_cloud.devices.return_value = [] result2 = await hass.config_entries.flow.async_configure(flow_id) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "no_devices" assert len(mock_lametric_cloud.devices.mock_calls) == 1 @@ -581,7 +581,7 @@ async def test_manual_errors( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "manual_entry" assert result2.get("errors") == {"base": reason} @@ -594,7 +594,7 @@ async def test_manual_errors( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -664,7 +664,7 @@ async def test_cloud_errors( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "cloud_select_device" assert result2.get("errors") == {"base": reason} @@ -678,7 +678,7 @@ async def test_cloud_errors( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -711,7 +711,7 @@ async def test_dhcp_discovery_updates_entry( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_API_KEY: "mock-from-fixture", @@ -737,7 +737,7 @@ async def test_dhcp_unknown_device( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unknown" @@ -791,7 +791,7 @@ async def test_reauth_cloud_import( result2 = await hass.config_entries.flow.async_configure(flow_id) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -855,7 +855,7 @@ async def test_reauth_cloud_abort_device_not_found( result2 = await hass.config_entries.flow.async_configure(flow_id) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_device_not_found" assert len(mock_lametric_cloud.devices.mock_calls) == 1 @@ -892,7 +892,7 @@ async def test_reauth_manual( flow_id, user_input={CONF_API_KEY: "mock-api-key"} ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -934,7 +934,7 @@ async def test_reauth_manual_sky( flow_id, user_input={CONF_API_KEY: "mock-api-key"} ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index d53a81a7edf..fe62d530719 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -49,7 +49,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -57,7 +57,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -65,7 +65,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": "/dev/ttyUSB0", @@ -85,14 +85,14 @@ async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], {"device": port.device} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": port.device, @@ -110,7 +110,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -118,7 +118,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -126,7 +126,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {"base": "cannot_connect"} @@ -142,14 +142,14 @@ async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], {"device": port.device} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -185,5 +185,5 @@ async def test_already_configured( result["flow_id"], {"device": port.device} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lastfm/test_config_flow.py b/tests/components/lastfm/test_config_flow.py index 93fc9e5a206..40710af3569 100644 --- a/tests/components/lastfm/test_config_flow.py +++ b/tests/components/lastfm/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from pylast import WSError import pytest -from homeassistant import data_entry_flow from homeassistant.components.lastfm.const import ( CONF_MAIN_USER, CONF_USERS, @@ -15,6 +14,7 @@ from homeassistant.components.lastfm.const import ( 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 . import ( API_KEY, @@ -42,14 +42,14 @@ async def test_full_user_flow(hass: HomeAssistant, default_user: MockUser) -> No result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "friends" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["options"] == CONF_DATA @@ -78,7 +78,7 @@ async def test_flow_fails( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_USER_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == message @@ -87,14 +87,14 @@ async def test_flow_fails( result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "friends" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["options"] == CONF_DATA @@ -112,7 +112,7 @@ async def test_flow_friends_invalid_username( result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "friends" with patch( @@ -124,7 +124,7 @@ async def test_flow_friends_invalid_username( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "friends" assert result["errors"]["base"] == "invalid_account" @@ -132,7 +132,7 @@ async def test_flow_friends_invalid_username( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["options"] == CONF_DATA @@ -153,7 +153,7 @@ async def test_flow_friends_no_friends( result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "friends" assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 @@ -171,7 +171,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -180,7 +180,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: API_KEY, CONF_MAIN_USER: USERNAME_1, @@ -201,7 +201,7 @@ async def test_options_flow_incorrect_username( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -216,7 +216,7 @@ async def test_options_flow_incorrect_username( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"]["base"] == "invalid_account" @@ -227,7 +227,7 @@ async def test_options_flow_incorrect_username( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: API_KEY, CONF_MAIN_USER: USERNAME_1, @@ -248,7 +248,7 @@ async def test_options_flow_from_import( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 @@ -266,6 +266,6 @@ async def test_options_flow_without_friends( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 80a634318b9..d3cee3b54e3 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.launch_library.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -17,7 +17,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -28,7 +28,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: {}, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -44,5 +44,5 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/laundrify/test_config_flow.py b/tests/components/laundrify/test_config_flow.py index 2583e8a5639..69a4b957cf5 100644 --- a/tests/components/laundrify/test_config_flow.py +++ b/tests/components/laundrify/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN, @@ -46,7 +46,7 @@ async def test_form_invalid_format( data={CONF_CODE: "invalidFormat"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_format"} @@ -59,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) - data=VALID_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_auth"} @@ -74,7 +74,7 @@ async def test_form_cannot_connect( data=VALID_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_form_unkown_exception( data=VALID_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -99,7 +99,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_REAUTH} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -108,7 +108,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_integration_already_exists(hass: HomeAssistant) -> None: @@ -125,5 +125,5 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/laundrify/test_init.py b/tests/components/laundrify/test_init.py index 84965af6768..e3ec54a3225 100644 --- a/tests/components/laundrify/test_init.py +++ b/tests/components/laundrify/test_init.py @@ -20,7 +20,7 @@ async def test_setup_entry_api_unauthorized( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) @@ -35,7 +35,7 @@ async def test_setup_entry_api_cannot_connect( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -46,7 +46,7 @@ async def test_setup_entry_successful(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_setup_entry_unload(hass: HomeAssistant) -> None: @@ -56,4 +56,4 @@ async def test_setup_entry_unload(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 1cf6c7f4b24..87115cb1900 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -114,13 +114,13 @@ async def test_lawn_mower_setup(hass: HomeAssistant) -> None: assert 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 is ConfigEntryState.LOADED assert hass.states.get(entity1.entity_id) assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED entity_state = hass.states.get(entity1.entity_id) assert entity_state diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index 2e50f20561e..6571b63ddf1 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -68,14 +68,13 @@ def create_config_entry(name): title = entry_data[CONF_HOST] unique_id = fixture_filename - entry = MockConfigEntry( + return MockConfigEntry( domain=DOMAIN, title=title, unique_id=unique_id, data=entry_data, options=options, ) - return entry @pytest.fixture diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index aa1b5086e65..e1705e4b349 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pypck.connection import PchkAuthenticationError, PchkLicenseError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.lcn.const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN from homeassistant.const import ( CONF_DEVICES, @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +48,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "pchk" assert result["data"] == IMPORT_DATA @@ -69,7 +70,7 @@ async def test_step_import_existing_host(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Check if config entry was updated - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "existing_configuration_updated" assert mock_entry.source == config_entries.SOURCE_IMPORT assert mock_entry.data == IMPORT_DATA @@ -95,5 +96,5 @@ async def test_step_import_error(hass: HomeAssistant, error, reason) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 292ebc045b2..670735439ce 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -20,12 +20,12 @@ from .conftest import MockPchkConnectionManager, setup_component async def test_async_setup_entry(hass: HomeAssistant, entry, lcn_connection) -> None: """Test a successful setup entry and unload of entry.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -36,7 +36,7 @@ async def test_async_setup_multiple_entries(hass: HomeAssistant, entry, entry2) 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 is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -44,7 +44,7 @@ async def test_async_setup_multiple_entries(hass: HomeAssistant, entry, entry2) assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -96,7 +96,7 @@ async def test_async_setup_entry_raises_authentication_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_raises_license_error( @@ -110,7 +110,7 @@ async def test_async_setup_entry_raises_license_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_raises_timeout_error( @@ -122,7 +122,7 @@ async def test_async_setup_entry_raises_timeout_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_from_configuration_yaml(hass: HomeAssistant) -> None: diff --git a/tests/components/ld2410_ble/test_config_flow.py b/tests/components/ld2410_ble/test_config_flow.py index 74e7e8a2c8e..78db43b3a23 100644 --- a/tests/components/ld2410_ble/test_config_flow.py +++ b/tests/components/ld2410_ble/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -45,7 +45,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, @@ -63,7 +63,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -84,7 +84,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -97,7 +97,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -113,7 +113,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -134,7 +134,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, @@ -152,7 +152,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -168,7 +168,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -189,7 +189,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, @@ -205,7 +205,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=LD2410_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -226,7 +226,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, diff --git a/tests/components/leaone/test_config_flow.py b/tests/components/leaone/test_config_flow.py index edacc3975c4..9712fce9c14 100644 --- a/tests/components/leaone/test_config_flow.py +++ b/tests/components/leaone/test_config_flow.py @@ -18,7 +18,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -32,14 +32,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.leaone.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "5F:5A:5C:52:D3:94"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TZC4 D394" assert result2["data"] == {} assert result2["result"].unique_id == "5F:5A:5C:52:D3:94" @@ -55,7 +55,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -69,7 +69,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "5F:5A:5C:52:D3:94"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -91,5 +91,5 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/led_ble/test_config_flow.py b/tests/components/led_ble/test_config_flow.py index 5ceda954ba8..c22c62e2fb1 100644 --- a/tests/components/led_ble/test_config_flow.py +++ b/tests/components/led_ble/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -49,7 +49,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LED_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -67,7 +67,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -88,7 +88,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -101,7 +101,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -117,7 +117,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -138,7 +138,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LED_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -156,7 +156,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -172,7 +172,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -193,7 +193,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LED_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -209,7 +209,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=LED_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -230,7 +230,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LED_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -246,5 +246,5 @@ async def test_bluetooth_unsupported_model(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=UNSUPPORTED_LED_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" diff --git a/tests/components/lg_netcast/__init__.py b/tests/components/lg_netcast/__init__.py new file mode 100644 index 00000000000..ce3e09aeb65 --- /dev/null +++ b/tests/components/lg_netcast/__init__.py @@ -0,0 +1,116 @@ +"""Tests for LG Netcast TV.""" + +from unittest.mock import patch +from xml.etree import ElementTree + +from pylgnetcast import AccessTokenError, LgNetCastClient, SessionIdError +import requests + +from homeassistant.components.lg_netcast import DOMAIN +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +FAIL_TO_BIND_IP = "1.2.3.4" + +IP_ADDRESS = "192.168.1.239" +DEVICE_TYPE = "TV" +MODEL_NAME = "MockLGModelName" +FRIENDLY_NAME = "LG Smart TV" +UNIQUE_ID = "1234" +ENTITY_ID = f"{MP_DOMAIN}.{MODEL_NAME.lower()}" + +FAKE_SESSION_ID = "987654321" +FAKE_PIN = "123456" + + +def _patched_lgnetcast_client( + *args, + session_error=False, + fail_connection: bool = True, + invalid_details: bool = False, + always_404: bool = False, + no_unique_id: bool = False, + **kwargs, +): + client = LgNetCastClient(*args, **kwargs) + + def _get_fake_session_id(): + if not client.access_token: + raise AccessTokenError("Fake Access Token Requested") + if session_error: + raise SessionIdError("Can not get session id from TV.") + return FAKE_SESSION_ID + + def _get_fake_query_device_info(): + if fail_connection: + raise requests.exceptions.ConnectTimeout("Mocked Failed Connection") + if always_404: + return None + if invalid_details: + raise ElementTree.ParseError("Mocked Parsed Error") + return { + "uuid": UNIQUE_ID if not no_unique_id else None, + "model_name": MODEL_NAME, + "friendly_name": FRIENDLY_NAME, + } + + client._get_session_id = _get_fake_session_id + client.query_device_info = _get_fake_query_device_info + + return client + + +def _patch_lg_netcast( + *, + session_error: bool = False, + fail_connection: bool = False, + invalid_details: bool = False, + always_404: bool = False, + no_unique_id: bool = False, +): + def _generate_fake_lgnetcast_client(*args, **kwargs): + return _patched_lgnetcast_client( + *args, + session_error=session_error, + fail_connection=fail_connection, + invalid_details=invalid_details, + always_404=always_404, + no_unique_id=no_unique_id, + **kwargs, + ) + + return patch( + "homeassistant.components.lg_netcast.config_flow.LgNetCastClient", + new=_generate_fake_lgnetcast_client, + ) + + +async def setup_lgnetcast(hass: HomeAssistant, unique_id: str = UNIQUE_ID): + """Initialize lg netcast and media_player for tests.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_MODEL: MODEL_NAME, + CONF_ID: unique_id, + }, + title=MODEL_NAME, + unique_id=unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/lg_netcast/conftest.py b/tests/components/lg_netcast/conftest.py new file mode 100644 index 00000000000..4faee2c6f06 --- /dev/null +++ b/tests/components/lg_netcast/conftest.py @@ -0,0 +1,11 @@ +"""Common fixtures and objects for the LG Netcast integration tests.""" + +import pytest + +from tests.common import async_mock_service + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") diff --git a/tests/components/lg_netcast/test_config_flow.py b/tests/components/lg_netcast/test_config_flow.py new file mode 100644 index 00000000000..c159b8fb9d2 --- /dev/null +++ b/tests/components/lg_netcast/test_config_flow.py @@ -0,0 +1,252 @@ +"""Define tests for the LG Netcast config flow.""" + +from datetime import timedelta +from unittest.mock import DEFAULT, patch + +from homeassistant import data_entry_flow +from homeassistant.components.lg_netcast.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) +from homeassistant.core import HomeAssistant + +from . import ( + FAKE_PIN, + FRIENDLY_NAME, + IP_ADDRESS, + MODEL_NAME, + UNIQUE_ID, + _patch_lg_netcast, +) + +from tests.common import MockConfigEntry + + +async def test_show_form(hass: HomeAssistant) -> None: + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + +async def test_user_invalid_host(hass: HomeAssistant) -> None: + """Test that errors are shown when the host is invalid.""" + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "invalid/host"} + ) + + assert result["errors"] == {CONF_HOST: "invalid_host"} + + +async def test_manual_host(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "authorize" + assert result2["errors"] is not None + assert result2["errors"][CONF_ACCESS_TOKEN] == "invalid_access_token" + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: FAKE_PIN} + ) + + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["title"] == FRIENDLY_NAME + assert result3["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: FRIENDLY_NAME, + CONF_MODEL: MODEL_NAME, + CONF_ID: UNIQUE_ID, + } + + +async def test_manual_host_no_connection_during_authorize(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(fail_connection=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_manual_host_invalid_details_during_authorize( + hass: HomeAssistant, +) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(invalid_details=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_manual_host_unsuccessful_details_response(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(always_404=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_manual_host_no_unique_id_response(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(no_unique_id=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "invalid_host" + + +async def test_invalid_session_id(hass: HomeAssistant) -> None: + """Test Invalid Session ID.""" + with _patch_lg_netcast(session_error=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: FAKE_PIN} + ) + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "authorize" + assert result2["errors"] is not None + assert result2["errors"]["base"] == "cannot_connect" + + +async def test_import(hass: HomeAssistant) -> None: + """Test that the import works.""" + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == UNIQUE_ID + assert result["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_MODEL: MODEL_NAME, + CONF_ID: UNIQUE_ID, + } + + +async def test_import_not_online(hass: HomeAssistant) -> None: + """Test that the import works.""" + with _patch_lg_netcast(fail_connection=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_import_duplicate_error(hass): + """Test that errors are shown when duplicates are added during import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=UNIQUE_ID, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_ID: UNIQUE_ID, + }, + ) + config_entry.add_to_hass(hass) + + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_ID: UNIQUE_ID, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_display_access_token_aborted(hass: HomeAssistant): + """Test Access token display is cancelled.""" + + def _async_track_time_interval( + hass: HomeAssistant, + action, + interval: timedelta, + *, + name=None, + cancel_on_shutdown=None, + ): + hass.async_create_task(action()) + return DEFAULT + + with ( + _patch_lg_netcast(session_error=True), + patch( + "homeassistant.components.lg_netcast.config_flow.async_track_time_interval" + ) as mock_interval, + ): + mock_interval.side_effect = _async_track_time_interval + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + assert not result["errors"] + + assert mock_interval.called + + hass.config_entries.flow.async_abort(result["flow_id"]) + assert mock_interval.return_value.called diff --git a/tests/components/lg_netcast/test_device_trigger.py b/tests/components/lg_netcast/test_device_trigger.py new file mode 100644 index 00000000000..05911acc41d --- /dev/null +++ b/tests/components/lg_netcast/test_device_trigger.py @@ -0,0 +1,148 @@ +"""The tests for LG NEtcast device triggers.""" + +import pytest + +from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.lg_netcast import DOMAIN, device_trigger +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import ENTITY_ID, UNIQUE_ID, setup_lgnetcast + +from tests.common import MockConfigEntry, async_get_device_automations + + +async def test_get_triggers( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test we get the expected triggers.""" + await setup_lgnetcast(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, UNIQUE_ID)}) + assert device is not None + + turn_on_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "lg_netcast.turn_on", + "device_id": device.id, + "metadata": {}, + } + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + assert turn_on_trigger in triggers + + +async def test_if_fires_on_turn_on_request( + hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry +) -> None: + """Test for turn_on triggers firing.""" + await setup_lgnetcast(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, UNIQUE_ID)}) + assert device is not None + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "lg_netcast.turn_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.device_id }}", + "id": "{{ trigger.id }}", + }, + }, + }, + { + "trigger": { + "platform": "lg_netcast.turn_on", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[0].data["some"] == device.id + assert calls[0].data["id"] == 0 + assert calls[1].data["some"] == ENTITY_ID + assert calls[1].data["id"] == 0 + + +async def test_failure_scenarios( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test failure scenarios.""" + await setup_lgnetcast(hass) + + # Test wrong trigger platform type + with pytest.raises(HomeAssistantError): + await device_trigger.async_attach_trigger( + hass, {"type": "wrong.type", "device_id": "invalid_device_id"}, None, {} + ) + + # Test invalid device id + with pytest.raises(HomeAssistantError): + await device_trigger.async_validate_trigger_config( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "lg_netcast.turn_on", + "device_id": "invalid_device_id", + }, + ) + + entry = MockConfigEntry(domain="fake", state=ConfigEntryState.LOADED, data={}) + entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, identifiers={("fake", "fake")} + ) + + config = { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "lg_netcast.turn_on", + } + + # Test that device id from non lg_netcast domain raises exception + with pytest.raises(InvalidDeviceAutomationConfig): + await device_trigger.async_validate_trigger_config(hass, config) + + # Test that only valid triggers are attached diff --git a/tests/components/lg_netcast/test_trigger.py b/tests/components/lg_netcast/test_trigger.py new file mode 100644 index 00000000000..e75dac501c3 --- /dev/null +++ b/tests/components/lg_netcast/test_trigger.py @@ -0,0 +1,189 @@ +"""The tests for LG Netcast device triggers.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import automation +from homeassistant.components.lg_netcast import DOMAIN +from homeassistant.const import SERVICE_RELOAD +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import ENTITY_ID, UNIQUE_ID, setup_lgnetcast + +from tests.common import MockEntity, MockEntityPlatform + + +async def test_lg_netcast_turn_on_trigger_device_id( + hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry +) -> None: + """Test for turn_on trigger by device_id firing.""" + await setup_lgnetcast(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, UNIQUE_ID)}) + assert device, repr(device_registry.devices) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.turn_on", + "device_id": device.id, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": device.id, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == device.id + assert calls[0].data["id"] == 0 + + with patch("homeassistant.config.load_yaml_dict", return_value={}): + await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) + + calls.clear() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_lg_netcast_turn_on_trigger_entity_id(hass: HomeAssistant, calls): + """Test for turn_on triggers by entity firing.""" + await setup_lgnetcast(hass) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.turn_on", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == ENTITY_ID + assert calls[0].data["id"] == 0 + + +async def test_wrong_trigger_platform_type( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test wrong trigger platform type.""" + await setup_lgnetcast(hass) + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.wrong_type", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + assert ( + "ValueError: Unknown LG Netcast TV trigger platform lg_netcast.wrong_type" + in caplog.text + ) + + +async def test_trigger_invalid_entity_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test turn on trigger using invalid entity_id.""" + await setup_lgnetcast(hass) + + platform = MockEntityPlatform(hass) + + invalid_entity = f"{DOMAIN}.invalid" + await platform.async_add_entities([MockEntity(name=invalid_entity)]) + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.turn_on", + "entity_id": invalid_entity, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + } + ], + }, + ) + + assert ( + f"ValueError: Entity {invalid_entity} is not a valid lg_netcast entity" + in caplog.text + ) diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index 4fab919c555..806c993e792 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -58,7 +59,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -83,7 +84,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -99,7 +100,7 @@ async def test_form_mac_info_response_empty(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -124,7 +125,7 @@ async def test_form_mac_info_response_empty(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -145,7 +146,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -172,7 +173,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -193,7 +194,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -224,7 +225,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -240,7 +241,7 @@ async def test_form_uuid_missing_from_mac_info(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -266,7 +267,7 @@ async def test_form_uuid_missing_from_mac_info(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -282,7 +283,7 @@ async def test_form_uuid_not_provided_by_api(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -311,7 +312,7 @@ async def test_form_uuid_not_provided_by_api(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id is None assert result2["data"] == { @@ -327,7 +328,7 @@ async def test_form_both_queues_empty(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -352,7 +353,7 @@ async def test_form_both_queues_empty(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_data"} assert len(mock_setup_entry.mock_calls) == 0 @@ -373,7 +374,7 @@ async def test_no_uuid_host_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -395,7 +396,7 @@ async def test_no_uuid_host_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -416,7 +417,7 @@ async def test_form_socket_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -437,7 +438,7 @@ async def test_form_os_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -474,5 +475,5 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/lidarr/test_config_flow.py b/tests/components/lidarr/test_config_flow.py index 01fa05ebb18..e44b03cd2a2 100644 --- a/tests/components/lidarr/test_config_flow.py +++ b/tests/components/lidarr/test_config_flow.py @@ -16,14 +16,14 @@ async def test_flow_user_form(hass: HomeAssistant, connection) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_INPUT, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -35,7 +35,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant, invalid_auth) -> None context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -48,7 +48,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, cannot_connect) -> data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -61,7 +61,7 @@ async def test_wrong_app(hass: HomeAssistant, wrong_app) -> None: data=MOCK_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "wrong_app" @@ -74,7 +74,7 @@ async def test_zeroconf_failed(hass: HomeAssistant, zeroconf_failed) -> None: data=MOCK_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "zeroconf_failed" @@ -89,7 +89,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -109,18 +109,18 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "abc123"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "abc123" diff --git a/tests/components/lidarr/test_init.py b/tests/components/lidarr/test_init.py index 48c5e3ff9a6..f10dc117b9c 100644 --- a/tests/components/lidarr/test_init.py +++ b/tests/components/lidarr/test_init.py @@ -14,7 +14,7 @@ async def test_setup( """Test setup.""" await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -30,7 +30,7 @@ async def test_async_setup_entry_not_ready( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -41,7 +41,7 @@ async def test_async_setup_entry_auth_failed( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index 0a0c26da424..59b7090788a 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -41,13 +41,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -55,13 +55,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -77,7 +77,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == {CONF_HOST: IP_ADDRESS} mock_setup.assert_called_once() @@ -87,7 +87,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -95,7 +95,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -106,13 +106,13 @@ async def test_discovery_but_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -122,7 +122,7 @@ async def test_discovery_but_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -140,7 +140,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -148,7 +148,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -157,7 +157,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -165,7 +165,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -177,7 +177,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: SERIAL} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_HOST: IP_ADDRESS, @@ -190,7 +190,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -198,7 +198,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +215,7 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -224,7 +224,7 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -238,7 +238,7 @@ async def test_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -253,7 +253,7 @@ async def test_manual(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == DEFAULT_ENTRY_TITLE assert result4["data"] == { CONF_HOST: IP_ADDRESS, @@ -272,7 +272,7 @@ async def test_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -281,7 +281,7 @@ async def test_manual_dns_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -312,7 +312,7 @@ async def test_manual_dns_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -322,7 +322,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -337,7 +337,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, } @@ -353,7 +353,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_config_flow_try_connect(): @@ -365,7 +365,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_config_flow_try_connect(): @@ -377,7 +377,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with ( @@ -392,7 +392,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -434,7 +434,7 @@ async def test_discovered_by_dhcp_or_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -448,7 +448,7 @@ async def test_discovered_by_dhcp_or_discovery( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, } @@ -496,7 +496,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -538,7 +538,7 @@ async def test_discovered_by_dhcp_or_homekit_updates_ip( data=data, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -548,7 +548,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -560,7 +560,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py index 3d0d127bf5c..42ece68a2c5 100644 --- a/tests/components/lifx/test_init.py +++ b/tests/components/lifx/test_init.py @@ -87,10 +87,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.LOADED + assert already_migrated_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + assert already_migrated_config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry(hass: HomeAssistant) -> None: @@ -106,7 +106,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: ): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_get_version_fails(hass: HomeAssistant) -> None: @@ -123,7 +123,7 @@ async def test_get_version_fails(hass: HomeAssistant) -> None: with _patch_discovery(device=bulb), _patch_device(device=bulb): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_dns_error_at_startup(hass: HomeAssistant) -> None: @@ -158,7 +158,7 @@ async def test_dns_error_at_startup(hass: HomeAssistant) -> None: ): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_wrong_serial( @@ -173,7 +173,7 @@ async def test_config_entry_wrong_serial( with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY assert ( "Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:c0, found aa:bb:cc:dd:ee:cc" diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 3f8ed8adbb6..d2a13f22253 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, @@ -168,7 +168,7 @@ async def test_get_action_capabilities( capabilities = await async_get_device_automation_capabilities( hass, DeviceAutomationType.ACTION, action ) - assert capabilities == {"extra_fields": []} or capabilities == {} + assert capabilities in ({"extra_fields": []}, {}) @pytest.mark.parametrize( diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 16237547bc9..eeee8530085 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, EntityCategory @@ -219,8 +219,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -238,8 +240,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -302,8 +306,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -367,9 +373,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ff692432d31..c38ab14061f 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory @@ -23,6 +23,14 @@ from tests.common import ( async_mock_service, ) +DATA_TEMPLATE_ATTRIBUTES = ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" +) + @pytest.fixture(autouse=True, name="stub_blueprint_populate") def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @@ -212,16 +220,7 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_on " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -236,16 +235,7 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_off " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -260,16 +250,7 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_on_or_off " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -332,16 +313,7 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_on " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -396,16 +368,7 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_off " + DATA_TEMPLATE_ATTRIBUTES }, }, } diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py index 1851b61fc15..9704268e650 100644 --- a/tests/components/linear_garage_door/test_config_flow.py +++ b/tests/components/linear_garage_door/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-site-name" assert result3["data"] == { "email": "test-email", @@ -86,7 +86,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=entry.data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user" with ( @@ -116,7 +116,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" entries = hass.config_entries.async_entries() @@ -153,7 +153,7 @@ async def test_form_invalid_login(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -176,5 +176,5 @@ async def test_form_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/linear_garage_door/test_coordinator.py b/tests/components/linear_garage_door/test_coordinator.py index 1e46d294f3f..be38b316c56 100644 --- a/tests/components/linear_garage_door/test_coordinator.py +++ b/tests/components/linear_garage_door/test_coordinator.py @@ -38,7 +38,7 @@ async def test_invalid_password( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows assert len(flows) == 1 @@ -70,4 +70,4 @@ async def test_invalid_login( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/linear_garage_door/test_cover.py b/tests/components/linear_garage_door/test_cover.py index e692d1867dc..9db7b80fd0e 100644 --- a/tests/components/linear_garage_door/test_cover.py +++ b/tests/components/linear_garage_door/test_cover.py @@ -32,7 +32,7 @@ async def test_data(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("cover.test_garage_1").state == STATE_OPEN assert hass.states.get("cover.test_garage_2").state == STATE_CLOSED assert hass.states.get("cover.test_garage_3").state == STATE_OPENING diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py index 32ebda7e125..63975c8bd3f 100644 --- a/tests/components/linear_garage_door/test_init.py +++ b/tests/components/linear_garage_door/test_init.py @@ -53,7 +53,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch( "homeassistant.components.linear_garage_door.coordinator.Linear.close", @@ -61,4 +61,4 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ): await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index b92aa59c9ce..7b8fa481b69 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION, DOMAIN from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -30,7 +31,7 @@ async def test_create_entry(hass: HomeAssistant, mock_litejet) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/test" assert result["data"] == test_data @@ -49,7 +50,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -64,7 +65,7 @@ async def test_flow_open_failed(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"][CONF_PORT] == "open_failed" @@ -75,7 +76,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -83,5 +84,5 @@ async def test_options(hass: HomeAssistant) -> None: user_input={CONF_DEFAULT_TRANSITION: 12}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index b9379efdad4..9746ab92cad 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -8,7 +8,7 @@ from unittest.mock import patch import pytest from homeassistant import setup -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index d516a3f14a2..5ffb78c7782 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant, mock_account) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant, mock_account) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME] assert result2["data"] == CONFIG[DOMAIN] assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +59,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: data=CONFIG[litterrobot.DOMAIN], ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -77,7 +77,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,7 +95,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -112,7 +112,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -134,7 +134,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -151,7 +151,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -174,7 +174,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -186,7 +186,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with ( @@ -203,6 +203,6 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/livisi/test_config_flow.py b/tests/components/livisi/test_config_flow.py index 7f4f8568030..9f492b9a45a 100644 --- a/tests/components/livisi/test_config_flow.py +++ b/tests/components/livisi/test_config_flow.py @@ -5,10 +5,10 @@ from unittest.mock import patch from aiolivisi import errors as livisi_errors import pytest -from homeassistant import data_entry_flow from homeassistant.components.livisi.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( VALID_CONFIG, @@ -30,7 +30,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SHC Classic" assert result["data"]["host"] == "1.1.1.1" assert result["data"]["password"] == "test" @@ -59,11 +59,11 @@ async def test_create_entity_after_login_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == expected_reason with mocked_livisi_login(), mocked_livisi_controller(), mocked_livisi_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/local_calendar/test_config_flow.py b/tests/components/local_calendar/test_config_flow.py index 89ea9d21ff5..c76fd9e283d 100644 --- a/tests/components/local_calendar/test_config_flow.py +++ b/tests/components/local_calendar/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Calendar" assert result2["data"] == { CONF_CALENDAR_NAME: "My Calendar", @@ -51,7 +51,7 @@ async def test_duplicate_name( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -63,5 +63,5 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/local_calendar/test_init.py b/tests/components/local_calendar/test_init.py index 8e79cccea36..8bb7a26d794 100644 --- a/tests/components/local_calendar/test_init.py +++ b/tests/components/local_calendar/test_init.py @@ -17,7 +17,7 @@ async def test_load_unload( ) -> None: """Test loading and unloading a config entry.""" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state @@ -26,7 +26,7 @@ async def test_load_unload( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(TEST_ENTITY) assert state assert state.state == "unavailable" @@ -54,7 +54,7 @@ async def test_load_failure( ) -> None: """Test failures loading the store.""" - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY state = hass.states.get(TEST_ENTITY) assert not state diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 82fcbf6d6e6..554163bbc1c 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -2,10 +2,10 @@ from __future__ import annotations -from homeassistant import data_entry_flow from homeassistant.components.local_ip.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,10 +15,10 @@ async def test_config_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get(f"sensor.{DOMAIN}") @@ -36,5 +36,5 @@ async def test_already_setup(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index 54126b21243..cc4f4dd4968 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -2,9 +2,9 @@ from __future__ import annotations -from homeassistant import config_entries from homeassistant.components.local_ip import DOMAIN from homeassistant.components.network import MDNS_TARGET_IP, async_get_source_ip +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -17,7 +17,7 @@ async def test_basic_setup(hass: HomeAssistant, mock_get_source_ip) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED local_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) state = hass.states.get(f"sensor.{DOMAIN}") @@ -26,4 +26,4 @@ async def test_basic_setup(hass: HomeAssistant, mock_get_source_ip) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/local_todo/test_config_flow.py b/tests/components/local_todo/test_config_flow.py index 381c97be167..b399753d286 100644 --- a/tests/components/local_todo/test_config_flow.py +++ b/tests/components/local_todo/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TODO_NAME assert result2["data"] == { "todo_list_name": TODO_NAME, @@ -49,7 +49,7 @@ async def test_duplicate_todo_list_name( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -61,5 +61,5 @@ async def test_duplicate_todo_list_name( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/local_todo/test_init.py b/tests/components/local_todo/test_init.py index 98da2ef3c12..c27c65c5706 100644 --- a/tests/components/local_todo/test_init.py +++ b/tests/components/local_todo/test_init.py @@ -17,7 +17,7 @@ async def test_load_unload( ) -> None: """Test loading and unloading a config entry.""" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state @@ -26,7 +26,7 @@ async def test_load_unload( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(TEST_ENTITY) assert state assert state.state == "unavailable" @@ -54,7 +54,7 @@ async def test_load_failure( ) -> None: """Test failures loading the todo store.""" - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY state = hass.states.get(TEST_ENTITY) assert not state diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 938892ad411..fdb38c68d6c 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -5,12 +5,13 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import locative from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -40,10 +41,10 @@ async def webhook_id(hass, locative_client): result = await hass.config_entries.flow.async_init( "locative", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 3396324284b..3b46117ccd2 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN, LockEntityFeature from homeassistant.const import EntityCategory diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 71e1b6ac48e..749e1037662 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN from homeassistant.const import ( diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index a45fd7527b5..3ad992d4458 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN from homeassistant.const import ( @@ -363,15 +363,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -388,15 +385,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -413,15 +407,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -438,15 +429,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 40e73a86c05..a75d83660a8 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import ANY, call, patch import pytest -import homeassistant.components.logentries as logentries +from homeassistant.components import logentries from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 3e30ea0ead0..d6df1f92a72 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -1,6 +1,7 @@ """The tests for the Logger component.""" from collections import defaultdict +import datetime import logging from typing import Any from unittest.mock import Mock, patch @@ -9,8 +10,12 @@ import pytest from homeassistant.components import logger from homeassistant.components.logger import LOGSEVERITY +from homeassistant.components.logger.helpers import SAVE_DELAY_LONG 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 HASS_NS = "unused.homeassistant" COMPONENTS_NS = f"{HASS_NS}.components" @@ -403,7 +408,7 @@ async def test_log_once_removed_from_store( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test logs with persistence "once" are removed from the store at startup.""" - hass_storage["core.logger"] = { + store_contents = { "data": { "logs": { ZONE_NS: {"type": "module", "level": "DEBUG", "persistence": "once"} @@ -412,7 +417,15 @@ async def test_log_once_removed_from_store( "key": "core.logger", "version": 1, } + hass_storage["core.logger"] = store_contents assert await async_setup_component(hass, "logger", {}) + assert hass_storage["core.logger"]["data"] == store_contents["data"] + + async_fire_time_changed( + hass, dt_util.utcnow() + datetime.timedelta(seconds=SAVE_DELAY_LONG) + ) + await hass.async_block_till_done() + assert hass_storage["core.logger"]["data"] == {"logs": {}} diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index f0de828c186..2525354598d 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.http import KEY_HASS from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( @@ -15,6 +15,7 @@ from homeassistant.components.logi_circle.config_flow import ( LogiCircleAuthCallbackView, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -67,7 +68,7 @@ async def test_step_import(hass: HomeAssistant, mock_logi_circle) -> None: flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -85,18 +86,18 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_logi_circle) - flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test-other"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "http://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Logi Circle ({})".format("testId") @@ -114,7 +115,7 @@ async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> Non flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -127,21 +128,21 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - with pytest.raises(data_entry_flow.AbortFlow): + with pytest.raises(AbortFlow): result = await flow.async_step_code() result = await flow.async_step_auth() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "external_setup" @@ -160,7 +161,7 @@ async def test_abort_if_authorize_fails( mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "external_error" result = await flow.async_step_auth() @@ -172,7 +173,7 @@ async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index e19bdc9fd73..cea0f969893 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -31,13 +31,11 @@ ZEROCONF_DATA = ZeroconfServiceInfo( def _mocked_climate() -> Climate: - climate = MagicMock(auto_spec=Climate) - return climate + return MagicMock(auto_spec=Climate) def _mocked_remote() -> Remote: - remote = MagicMock(auto_spec=Remote) - return remote + return MagicMock(auto_spec=Remote) def _mocked_device() -> Device: diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index 18cbe33db3a..1eddbe23b24 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -31,7 +31,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -44,7 +44,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: IP_ADDRESS} assert result["title"] == DEFAULT_ENTRY_TITLE assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +59,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -69,7 +69,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +78,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -88,7 +88,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -97,7 +97,7 @@ async def test_manual_setup_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -107,7 +107,7 @@ async def test_manual_setup_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -122,7 +122,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -134,7 +134,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_HOST: IP_ADDRESS} assert result2["title"] == DEFAULT_ENTRY_TITLE assert mock_async_setup_entry.called @@ -156,7 +156,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.2" @@ -172,7 +172,7 @@ async def test_discovered_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -187,5 +187,5 @@ async def test_discovered_zeroconf_unknown_exception(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/loqed/test_config_flow.py b/tests/components/loqed/test_config_flow.py index ed18f0ae40e..d59ed60796b 100644 --- a/tests/components/loqed/test_config_flow.py +++ b/tests/components/loqed/test_config_flow.py @@ -42,7 +42,7 @@ async def test_create_entry_zeroconf(hass: HomeAssistant) -> None: data=zeroconf_data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_lock = Mock(spec=loqed.Lock, id="Foo") @@ -76,7 +76,7 @@ async def test_create_entry_zeroconf(hass: HomeAssistant) -> None: await hass.async_block_till_done() found_lock = all_locks_response["data"][0] - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "LOQED Touch Smart Lock" assert result2["data"] == { "id": "Foo", @@ -101,7 +101,7 @@ async def test_create_entry_user( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None lock_result = json.loads(load_fixture("loqed/status_ok.json")) @@ -137,7 +137,7 @@ async def test_create_entry_user( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "LOQED Touch Smart Lock" assert result2["data"] == { "id": "Foo", @@ -162,7 +162,7 @@ async def test_cannot_connect( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -175,7 +175,7 @@ async def test_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -188,7 +188,7 @@ async def test_invalid_auth_when_lock_not_found( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) @@ -203,7 +203,7 @@ async def test_invalid_auth_when_lock_not_found( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -216,7 +216,7 @@ async def test_cannot_connect_when_lock_not_reachable( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) @@ -236,5 +236,5 @@ async def test_cannot_connect_when_lock_not_reachable( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 7469fe6e486..ea9b6211823 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -25,7 +25,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_duplicate_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -45,7 +45,7 @@ async def test_communication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_luftdaten.get_data.side_effect = LuftdatenConnectionError @@ -54,7 +54,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} @@ -64,7 +64,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -78,7 +78,7 @@ async def test_invalid_sensor(hass: HomeAssistant, mock_luftdaten: MagicMock) -> DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_luftdaten.validate_sensor.return_value = False @@ -87,7 +87,7 @@ async def test_invalid_sensor(hass: HomeAssistant, mock_luftdaten: MagicMock) -> user_input={CONF_SENSOR_ID: 11111}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} @@ -97,7 +97,7 @@ async def test_invalid_sensor(hass: HomeAssistant, mock_luftdaten: MagicMock) -> user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -114,7 +114,7 @@ async def test_step_user( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -125,7 +125,7 @@ async def test_step_user( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SENSOR_ID: 12345, diff --git a/tests/components/lupusec/test_config_flow.py b/tests/components/lupusec/test_config_flow.py index 2ca313139dc..e106bbd5001 100644 --- a/tests/components/lupusec/test_config_flow.py +++ b/tests/components/lupusec/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -63,7 +63,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_DATA_STEP[CONF_HOST] assert result2["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +85,7 @@ async def test_flow_user_init_data_error_and_recover( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -98,7 +98,7 @@ async def test_flow_user_init_data_error_and_recover( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": text_error} assert len(mock_initialize_lupusec.mock_calls) == 1 @@ -120,7 +120,7 @@ async def test_flow_user_init_data_error_and_recover( await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == MOCK_DATA_STEP[CONF_HOST] assert result3["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -151,7 +151,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -183,7 +183,7 @@ async def test_flow_source_import( await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == mock_title assert result["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -214,7 +214,7 @@ async def test_flow_source_import_error_and_recover( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == text_error assert len(mock_initialize_lupusec.mock_calls) == 1 @@ -236,5 +236,5 @@ async def test_flow_source_import_already_configured(hass: HomeAssistant) -> Non data=MOCK_IMPORT_STEP, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lutron/test_config_flow.py b/tests/components/lutron/test_config_flow.py index db3faa7f911..e4904838e1a 100644 --- a/tests/components/lutron/test_config_flow.py +++ b/tests/components/lutron/test_config_flow.py @@ -27,7 +27,7 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -39,7 +39,7 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "Lutron" assert result["data"] == MOCK_DATA_STEP @@ -63,7 +63,7 @@ async def test_flow_failure( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -75,7 +75,7 @@ async def test_flow_failure( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": text_error} with ( @@ -87,7 +87,7 @@ async def test_flow_failure( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "Lutron" assert result["data"] == MOCK_DATA_STEP @@ -101,7 +101,7 @@ async def test_flow_incorrect_guid( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -113,7 +113,7 @@ async def test_flow_incorrect_guid( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with ( @@ -125,7 +125,7 @@ async def test_flow_incorrect_guid( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_flow_single_instance_allowed(hass: HomeAssistant) -> None: @@ -137,7 +137,7 @@ async def test_flow_single_instance_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -162,7 +162,7 @@ async def test_import( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == MOCK_DATA_IMPORT assert len(mock_setup_entry.mock_calls) == 1 @@ -187,7 +187,7 @@ async def test_import_flow_failure( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -202,7 +202,7 @@ async def test_import_flow_guid_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -218,5 +218,5 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 15a4fca7d33..b2edaa07155 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -9,7 +9,7 @@ from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY from pylutron_caseta.smartbridge import Smartbridge import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.lutron_caseta import DOMAIN import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow @@ -22,6 +22,7 @@ from homeassistant.components.lutron_caseta.const import ( ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ENTRY_MOCK_DATA, MockBridge @@ -74,7 +75,7 @@ async def test_bridge_import_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CasetaConfigFlow.ENTRY_DEFAULT_TITLE assert result["data"] == entry_mock_data assert result["result"].unique_id == "000004d2" @@ -101,13 +102,13 @@ async def test_bridge_cannot_connect(hass: HomeAssistant) -> None: data=entry_mock_data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -124,13 +125,13 @@ async def test_bridge_cannot_connect_unknown_error(hass: HomeAssistant) -> None: data=EMPTY_MOCK_CONFIG_ENTRY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -144,13 +145,13 @@ async def test_bridge_invalid_ssl_error(hass: HomeAssistant) -> None: data=EMPTY_MOCK_CONFIG_ENTRY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -171,7 +172,7 @@ async def test_duplicate_bridge_import(hass: HomeAssistant) -> None: data=ENTRY_MOCK_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -194,7 +195,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: CONF_CA_CERTS: "", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: @@ -206,7 +207,7 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -217,7 +218,7 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -239,7 +240,7 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "1.1.1.1" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -260,7 +261,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -271,7 +272,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -293,7 +294,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -310,7 +311,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -321,7 +322,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -343,7 +344,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "1.1.1.1" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -365,7 +366,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -379,7 +380,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -395,7 +396,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "1.1.1.1" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -432,7 +433,7 @@ async def test_zeroconf_host_already_configured( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -460,7 +461,7 @@ async def test_zeroconf_lutron_id_already_configured(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.1.1.1" @@ -483,7 +484,7 @@ async def test_zeroconf_not_lutron_device(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_lutron_device" @@ -511,7 +512,7 @@ async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with ( @@ -533,7 +534,7 @@ async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "abc" assert result2["data"] == { CONF_HOST: "1.1.1.1", diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 00d623ea3ce..73b3aae2d3d 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -5,13 +5,15 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -39,7 +41,7 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -62,7 +64,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -104,7 +106,7 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -163,7 +165,7 @@ async def test_reauthentication_flow( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 8b83f2b0ec7..296a4fbfa6b 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -9,7 +9,7 @@ from aiohttp.test_utils import TestClient import pytest from homeassistant.bootstrap import async_setup_component -import homeassistant.components.mailbox as mailbox +from homeassistant.components import mailbox from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index a36051bd102..e2274f03d23 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -5,11 +5,12 @@ import hmac import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import mailgun, webhook from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_API_KEY, CONF_DOMAIN from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component API_KEY = "abc123" @@ -38,10 +39,10 @@ async def webhook_id_with_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] @@ -58,10 +59,10 @@ async def webhook_id_without_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index b3fefe3ac67..18227914df4 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -84,14 +84,12 @@ class _MockAsyncClient(AsyncClient): return RoomResolveAliasResponse( room_alias=room_alias, room_id=room_id, servers=[TEST_HOMESERVER] ) - else: - return RoomResolveAliasError(message=f"Could not resolve {room_alias}") + return RoomResolveAliasError(message=f"Could not resolve {room_alias}") async def join(self, room_id: RoomID): if room_id in TEST_JOINABLE_ROOMS.values(): return JoinResponse(room_id=room_id) - else: - return JoinError(message="Not allowed to join this room.") + return JoinError(message="Not allowed to join this room.") async def login(self, *args, **kwargs): if kwargs.get("password") == TEST_PASSWORD or kwargs.get("token") == TEST_TOKEN: @@ -101,9 +99,8 @@ class _MockAsyncClient(AsyncClient): device_id="test_device", user_id=TEST_MXID, ) - else: - self.access_token = "" - return LoginError(message="LoginError", status_code="status_code") + self.access_token = "" + return LoginError(message="LoginError", status_code="status_code") async def logout(self, *args, **kwargs): self.access_token = "" @@ -115,19 +112,17 @@ class _MockAsyncClient(AsyncClient): return WhoamiResponse( user_id=TEST_MXID, device_id=TEST_DEVICE_ID, is_guest=False ) - else: - self.access_token = "" - return WhoamiError( - message="Invalid access token passed.", status_code="M_UNKNOWN_TOKEN" - ) + self.access_token = "" + return WhoamiError( + message="Invalid access token passed.", status_code="M_UNKNOWN_TOKEN" + ) async def room_send(self, *args, **kwargs): if not self.logged_in: raise LocalProtocolError if kwargs["room_id"] not in TEST_JOINABLE_ROOMS.values(): return ErrorResponse(message="Cannot send a message in this room.") - else: - return Response() + return Response() async def sync(self, *args, **kwargs): return None @@ -286,6 +281,9 @@ async def matrix_bot( assert await async_setup_component(hass, MATRIX_DOMAIN, MOCK_CONFIG_DATA) assert await async_setup_component(hass, NOTIFY_DOMAIN, MOCK_CONFIG_DATA) await hass.async_block_till_done() + + # Accessing hass.data in tests is not desirable, but all the tests here + # currently do this. assert isinstance(matrix_bot := hass.data[MATRIX_DOMAIN], MatrixBot) await hass.async_start() diff --git a/tests/components/matrix/test_rooms.py b/tests/components/matrix/test_rooms.py index 29081b80fd5..66d1afbf532 100644 --- a/tests/components/matrix/test_rooms.py +++ b/tests/components/matrix/test_rooms.py @@ -1,14 +1,36 @@ """Test MatrixBot._join.""" +import pytest + from homeassistant.components.matrix import MatrixBot +from homeassistant.components.matrix.const import DOMAIN as MATRIX_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .conftest import MOCK_CONFIG_DATA from tests.components.matrix.conftest import TEST_BAD_ROOM, TEST_JOINABLE_ROOMS -async def test_join(hass, matrix_bot: MatrixBot, caplog): +async def test_join( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_client, + mock_save_json, + mock_allowed_path, +) -> None: """Test joining configured rooms.""" + assert await async_setup_component(hass, MATRIX_DOMAIN, MOCK_CONFIG_DATA) + assert await async_setup_component(hass, NOTIFY_DOMAIN, MOCK_CONFIG_DATA) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done(wait_background_tasks=True) + + # Accessing hass.data in tests is not desirable, but all the tests here + # currently do this. + matrix_bot = hass.data[MATRIX_DOMAIN] - await hass.async_start() for room_id in TEST_JOINABLE_ROOMS: assert f"Joined or already in room '{room_id}'" in caplog.messages @@ -21,7 +43,7 @@ async def test_join(hass, matrix_bot: MatrixBot, caplog): ) -async def test_resolve_aliases(hass, matrix_bot: MatrixBot): +async def test_resolve_aliases(hass: HomeAssistant, matrix_bot: MatrixBot) -> None: """Test resolving configured room aliases into room ids.""" await hass.async_start() diff --git a/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json b/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json new file mode 100644 index 00000000000..c1264f5b7ea --- /dev/null +++ b/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json @@ -0,0 +1,244 @@ +{ + "node_id": 8, + "date_commissioned": "2024-03-07T01:39:20.590755", + "last_interview": "2024-04-02T14:16:31.045880", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 54, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "254": 1 + }, + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 2 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 3, + "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": 1, + "0/40/1": "Leviton", + "0/40/2": 4251, + "0/40/3": "D215S", + "0/40/4": 4097, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 131365, + "0/40/10": "2.1.25", + "0/40/15": "12345678", + "0/40/18": "abcdefgh", + "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, 15, 18, 19, 65528, 65529, 65531, 65532, + 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/43/0": "en-US", + "0/43/1": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "0/44/0": 0, + "0/44/1": 0, + "0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7], + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 1, 2, 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": 1, + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/7": null, + "0/49/65532": 1, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/1": 1, + "0/51/2": 2380987, + "0/51/3": 661, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/52/1": 49792, + "0/52/2": 262528, + "0/52/3": 272704, + "0/52/65532": 1, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [0], + "0/52/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/54/0": "blah", + "0/54/1": 4, + "0/54/2": 3, + "0/54/3": 11, + "0/54/4": -43, + "0/54/65532": 0, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [], + "0/54/65531": [0, 1, 2, 3, 4, 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/5": 2, + "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": 3, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "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": 2, + "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/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/6/0": false, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": null, + "1/6/65532": 1, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ], + "1/8/0": 254, + "1/8/1": 0, + "1/8/2": 1, + "1/8/3": 254, + "1/8/15": 0, + "1/8/16": 0, + "1/8/17": null, + "1/8/20": 50, + "1/8/16384": null, + "1/8/65532": 3, + "1/8/65533": 5, + "1/8/65528": [], + "1/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "1/8/65531": [ + 0, 1, 2, 3, 15, 16, 17, 20, 16384, 65528, 65529, 65531, 65532, 65533 + ], + "1/29/0": [ + { + "0": 256, + "1": 1 + } + ], + "1/29/1": [3, 4, 6, 8, 29], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index e690844c228..39ae40172c1 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Generator +from ipaddress import ip_address from typing import Any from unittest.mock import DEFAULT, AsyncMock, MagicMock, call, patch @@ -12,6 +13,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.hassio import HassioAPIError, HassioServiceInfo from homeassistant.components.matter.const import ADDON_SLUG, DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -22,6 +24,37 @@ ADDON_DISCOVERY_INFO = { "host": "host1", "port": 5581, } +ZEROCONF_INFO_TCP = ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matter._tcp.local.", + name="ABCDEFGH123456789-0000000012345678._matter._tcp.local.", + properties={"SII": "3300", "SAI": "1100", "T": "0"}, +) + +ZEROCONF_INFO_UDP = ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matterc._udp.local.", + name="ABCDEFGH123456789._matterc._udp.local.", + properties={ + "VP": "4874+77", + "DT": "21", + "DN": "Eve Door", + "SII": "3300", + "SAI": "1100", + "T": "0", + "D": "183", + "CM": "2", + "RI": "0400530980B950D59BF473CFE42BD7DDBF2D", + "PH": "36", + "PI": None, + }, +) @pytest.fixture(name="setup_entry", autouse=True) @@ -33,6 +66,15 @@ def setup_entry_fixture() -> Generator[AsyncMock, None, None]: yield mock_setup_entry +@pytest.fixture(name="unload_entry", autouse=True) +def unload_entry_fixture() -> Generator[AsyncMock, None, None]: + """Mock entry unload.""" + with patch( + "homeassistant.components.matter.async_unload_entry", return_value=True + ) as mock_unload_entry: + yield mock_unload_entry + + @pytest.fixture(name="client_connect", autouse=True) def client_connect_fixture() -> Generator[AsyncMock, None, None]: """Mock server version.""" @@ -78,6 +120,16 @@ def addon_setup_time_fixture() -> Generator[int, None, None]: yield addon_setup_time +@pytest.fixture(name="not_onboarded") +def mock_onboarded_fixture() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is not yet onboarded.""" + with patch( + "homeassistant.components.matter.config_flow.async_is_onboarded", + return_value=False, + ) as mock_onboarded: + yield mock_onboarded + + async def test_manual_create_entry( hass: HomeAssistant, client_connect: AsyncMock, @@ -87,7 +139,7 @@ async def test_manual_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -99,7 +151,7 @@ async def test_manual_create_entry( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5580/ws", @@ -137,7 +189,7 @@ async def test_manual_errors( ) assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} @@ -156,7 +208,7 @@ async def test_manual_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -168,7 +220,7 @@ async def test_manual_already_configured( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://localhost:5580/ws" assert entry.data["use_addon"] is False @@ -177,6 +229,221 @@ async def test_manual_already_configured( assert setup_entry.call_count == 1 +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_zeroconf_discovery( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow started from Zeroconf discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "integration_created_addon": False, + "use_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_zeroconf_discovery_not_onboarded_not_supervisor( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow started from Zeroconf discovery when not onboarded.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "integration_created_addon": False, + "use_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_already_discovered( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_running: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and already discovered.""" + result_flow_1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + result_flow_2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + assert result_flow_2["type"] is FlowResultType.ABORT + assert result_flow_2["reason"] == "already_configured" + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result_flow_1["type"] is FlowResultType.CREATE_ENTRY + assert result_flow_1["title"] == "Matter" + assert result_flow_1["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_running( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_running: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_installed: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_not_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + addon_not_installed: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on not installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 2 + assert install_addon.call_args == call(hass, "core_matter_server") + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": True, + } + assert setup_entry.call_count == 1 + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_supervisor_discovery( hass: HomeAssistant, @@ -203,7 +470,7 @@ async def test_supervisor_discovery( assert addon_info.call_count == 1 assert client_connect.call_count == 0 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -238,13 +505,13 @@ async def test_supervisor_discovery_addon_info_failed( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_info_failed" @@ -269,14 +536,14 @@ async def test_clean_supervisor_discovery_on_user_create( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -284,7 +551,7 @@ async def test_clean_supervisor_discovery_on_user_create( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( @@ -297,7 +564,7 @@ async def test_clean_supervisor_discovery_on_user_create( assert len(hass.config_entries.flow.async_progress()) == 0 assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5580/ws", @@ -333,7 +600,7 @@ async def test_abort_supervisor_discovery_with_existing_entry( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -348,7 +615,7 @@ async def test_abort_supervisor_discovery_with_existing_flow( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_init( @@ -363,7 +630,7 @@ async def test_abort_supervisor_discovery_with_existing_flow( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -390,7 +657,7 @@ async def test_abort_supervisor_discovery_for_other_addon( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_matter_addon" @@ -417,12 +684,12 @@ async def test_supervisor_discovery_addon_not_running( assert addon_info.call_count == 0 assert result["step_id"] == "hassio_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -431,7 +698,7 @@ async def test_supervisor_discovery_addon_not_running( assert start_addon.call_args == call(hass, "core_matter_server") assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -467,20 +734,20 @@ async def test_supervisor_discovery_addon_not_installed( assert addon_info.call_count == 0 assert addon_store_info.call_count == 0 assert result["step_id"] == "hassio_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert addon_info.call_count == 0 assert addon_store_info.call_count == 1 assert result["step_id"] == "install_addon" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert install_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -489,7 +756,7 @@ async def test_supervisor_discovery_addon_not_installed( assert start_addon.call_args == call(hass, "core_matter_server") assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -510,14 +777,14 @@ async def test_not_addon( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( @@ -529,7 +796,7 @@ async def test_not_addon( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5581/ws", @@ -553,7 +820,7 @@ async def test_addon_running( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -563,7 +830,7 @@ async def test_addon_running( assert addon_info.call_count == 1 assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -644,7 +911,7 @@ async def test_addon_running_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -654,7 +921,91 @@ async def test_addon_running_failures( assert addon_info.call_count == 1 assert get_addon_discovery_info.called is discovery_info_called assert client_connect.called is client_connect_called - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == abort_reason + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize( + ( + "discovery_info", + "discovery_info_error", + "client_connect_error", + "addon_info_error", + "abort_reason", + "discovery_info_called", + "client_connect_called", + ), + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + None, + "cannot_connect", + True, + True, + ), + ( + None, + None, + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + None, + HassioAPIError(), + "addon_info_failed", + False, + False, + ), + ], +) +async def test_addon_running_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + discovery_info_error: Exception | None, + client_connect_error: Exception | None, + addon_info_error: Exception | None, + abort_reason: str, + discovery_info_called: bool, + client_connect_called: bool, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test all failures when add-on is running and not onboarded.""" + get_addon_discovery_info.side_effect = discovery_info_error + client_connect.side_effect = client_connect_error + addon_info.side_effect = addon_info_error + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason @@ -680,7 +1031,7 @@ async def test_addon_running_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -689,7 +1040,7 @@ async def test_addon_running_already_configured( await hass.async_block_till_done() assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://host1:5581/ws" assert entry.title == "Matter" @@ -710,7 +1061,7 @@ async def test_addon_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -718,7 +1069,7 @@ async def test_addon_installed( ) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -726,7 +1077,7 @@ async def test_addon_installed( await hass.async_block_till_done() assert start_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -789,7 +1140,7 @@ async def test_addon_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -797,7 +1148,7 @@ async def test_addon_installed_failures( ) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -806,7 +1157,72 @@ async def test_addon_installed_failures( assert start_addon.call_args == call(hass, "core_matter_server") assert get_addon_discovery_info.called is discovery_info_called assert client_connect.called is client_connect_called - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize( + ( + "discovery_info", + "start_addon_error", + "client_connect_error", + "discovery_info_called", + "client_connect_called", + ), + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + False, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + True, + True, + ), + ( + None, + None, + None, + True, + False, + ), + ], +) +async def test_addon_installed_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + start_addon_error: Exception | None, + client_connect_error: Exception | None, + discovery_info_called: bool, + client_connect_called: bool, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test add-on start failure when add-on is installed and not onboarded.""" + start_addon.side_effect = start_addon_error + client_connect.side_effect = client_connect_error + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -833,7 +1249,7 @@ async def test_addon_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -841,7 +1257,7 @@ async def test_addon_installed_already_configured( ) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -849,7 +1265,7 @@ async def test_addon_installed_already_configured( await hass.async_block_till_done() assert start_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://host1:5581/ws" assert entry.title == "Matter" @@ -872,7 +1288,7 @@ async def test_addon_not_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -881,7 +1297,7 @@ async def test_addon_not_installed( assert addon_info.call_count == 0 assert addon_store_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -889,7 +1305,7 @@ async def test_addon_not_installed( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert install_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -897,7 +1313,7 @@ async def test_addon_not_installed( await hass.async_block_till_done() assert start_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -921,14 +1337,14 @@ async def test_addon_not_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -937,7 +1353,31 @@ async def test_addon_not_installed_failures( assert install_addon.call_args == call(hass, "core_matter_server") assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_addon_not_installed_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test add-on install failure.""" + install_addon.side_effect = HassioAPIError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info + ) + await hass.async_block_till_done() + + assert install_addon.call_args == call(hass, "core_matter_server") + assert addon_info.call_count == 0 + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -967,7 +1407,7 @@ async def test_addon_not_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -976,7 +1416,7 @@ async def test_addon_not_installed_already_configured( assert addon_info.call_count == 0 assert addon_store_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -984,7 +1424,7 @@ async def test_addon_not_installed_already_configured( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert install_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -993,7 +1433,7 @@ async def test_addon_not_installed_already_configured( assert start_addon.call_args == call(hass, "core_matter_server") assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://host1:5581/ws" assert entry.title == "Matter" diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index 327e73dd4de..4472e712b20 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -68,7 +68,7 @@ async def test_entry_setup_unload( await hass.async_block_till_done() assert matter_client.connect.call_count == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED entity_state = hass.states.get("light.mock_onoff_light") assert entity_state assert entity_state.state != STATE_UNAVAILABLE @@ -76,7 +76,7 @@ async def test_entry_setup_unload( await hass.config_entries.async_unload(entry.entry_id) assert matter_client.disconnect.call_count == 1 - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED entity_state = hass.states.get("light.mock_onoff_light") assert entity_state assert entity_state.state == STATE_UNAVAILABLE @@ -210,12 +210,12 @@ async def test_listen_failure_config_entry_loaded( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED listen_block.set() await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert matter_client.disconnect.call_count == 1 diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 9c3c2610d92..775790701d1 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -18,21 +18,31 @@ from .common import ( # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize( - ("fixture", "entity_id"), + ("fixture", "entity_id", "supported_color_modes"), [ - ("extended-color-light", "light.mock_extended_color_light"), - ("color-temperature-light", "light.mock_color_temperature_light"), - ("dimmable-light", "light.mock_dimmable_light"), - ("onoff-light", "light.mock_onoff_light"), + ( + "extended-color-light", + "light.mock_extended_color_light", + ["color_temp", "hs", "xy"], + ), + ( + "color-temperature-light", + "light.mock_color_temperature_light", + ["color_temp"], + ), + ("dimmable-light", "light.mock_dimmable_light", ["brightness"]), + ("onoff-light", "light.mock_onoff_light", ["onoff"]), + ("onoff-light-with-levelcontrol-present", "light.d215s", ["onoff"]), ], ) -async def test_on_off_light( +async def test_light_turn_on_off( hass: HomeAssistant, matter_client: MagicMock, fixture: str, entity_id: str, + supported_color_modes: list[str], ) -> None: - """Test an on/off light.""" + """Test basic light discovery and turn on/off.""" light_node = await setup_integration_with_node_fixture( hass, @@ -48,6 +58,11 @@ async def test_on_off_light( assert state is not None assert state.state == "off" + # check the supported_color_modes + # especially important is the onoff light device type that does have + # a levelcontrol cluster present which we should ignore + assert state.attributes["supported_color_modes"] == supported_color_modes + # Test that the light is on set_node_attribute(light_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 6d294259c01..b8c1be15268 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch from meater import AuthenticationError, ServiceUnavailableError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.meater import DOMAIN 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 @@ -39,7 +40,7 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -85,7 +86,7 @@ async def test_user_flow(hass: HomeAssistant, mock_meater) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -95,7 +96,7 @@ async def test_user_flow(hass: HomeAssistant, mock_meater) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], conf) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123", @@ -128,7 +129,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_meater) -> None: data=data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is None @@ -138,7 +139,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_meater) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" config_entry = hass.config_entries.async_entries(DOMAIN)[0] diff --git a/tests/components/medcom_ble/test_config_flow.py b/tests/components/medcom_ble/test_config_flow.py index ca75dfd80ab..b470245978d 100644 --- a/tests/components/medcom_ble/test_config_flow.py +++ b/tests/components/medcom_ble/test_config_flow.py @@ -31,7 +31,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: data=MEDCOM_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == {"name": "InspectorBLE-D9A0"} @@ -52,7 +52,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: result["flow_id"], user_input={"not": "empty"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "InspectorBLE-D9A0" assert result["result"].unique_id == "a0:d9:5a:57:0b:00" @@ -69,7 +69,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=MEDCOM_DEVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -82,7 +82,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -113,7 +113,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "InspectorBLE-D9A0" assert result["result"].unique_id == "a0:d9:5a:57:0b:00" @@ -127,7 +127,7 @@ async def test_user_setup_no_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -145,7 +145,7 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -154,7 +154,7 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -167,7 +167,7 @@ async def test_user_setup_unknown_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -180,7 +180,7 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -193,7 +193,7 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -206,7 +206,7 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -224,5 +224,5 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/media_extractor/__init__.py b/tests/components/media_extractor/__init__.py index 7aac726501b..79130f1ea4b 100644 --- a/tests/components/media_extractor/__init__.py +++ b/tests/components/media_extractor/__init__.py @@ -36,9 +36,13 @@ class MockYoutubeDL: """Initialize mock object for YoutubeDL.""" self.params = params - def extract_info(self, url: str, *, process: bool = False) -> dict[str, Any]: + def extract_info( + self, url: str, *, download: bool = True, process: bool = False + ) -> dict[str, Any]: """Return info.""" self._fixture = _get_base_fixture(url) + if not download: + return load_json_object_fixture(f"media_extractor/{self._fixture}.json") return load_json_object_fixture(f"media_extractor/{self._fixture}_info.json") def process_ie_result( diff --git a/tests/components/media_extractor/fixtures/no_formats.json b/tests/components/media_extractor/fixtures/no_formats.json new file mode 100644 index 00000000000..aefb1525738 --- /dev/null +++ b/tests/components/media_extractor/fixtures/no_formats.json @@ -0,0 +1,87 @@ +{ + "id": "223644256", + "uploader": "BRUTTOBAND", + "uploader_id": "111488150", + "uploader_url": "https://soundcloud.com/bruttoband", + "timestamp": 1442140228, + "title": "BRUTTO - \u0420\u043e\u0434\u043d\u044b \u043a\u0440\u0430\u0439", + "description": "", + "thumbnails": [ + { + "id": "mini", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-mini.jpg", + "width": 16, + "height": 16 + }, + { + "id": "tiny", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-tiny.jpg", + "width": 20, + "height": 20 + }, + { + "id": "small", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-small.jpg", + "width": 32, + "height": 32 + }, + { + "id": "badge", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-badge.jpg", + "width": 47, + "height": 47 + }, + { + "id": "t67x67", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t67x67.jpg", + "width": 67, + "height": 67 + }, + { + "id": "large", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-large.jpg", + "width": 100, + "height": 100 + }, + { + "id": "t300x300", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t300x300.jpg", + "width": 300, + "height": 300 + }, + { + "id": "crop", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-crop.jpg", + "width": 400, + "height": 400 + }, + { + "id": "t500x500", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t500x500.jpg", + "width": 500, + "height": 500 + }, + { + "id": "original", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-original.jpg", + "preference": 10 + } + ], + "duration": 229.089, + "webpage_url": "https://soundcloud.com/bruttoband/brutto-11", + "license": "all-rights-reserved", + "view_count": 291779, + "like_count": 3347, + "comment_count": 14, + "repost_count": 59, + "genre": "Brutto", + "url": "https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "mp3", + "original_url": "https://soundcloud.com/bruttoband/brutto-11", + "webpage_url_basename": "brutto-11", + "webpage_url_domain": "soundcloud.com", + "extractor": "soundcloud", + "extractor_key": "Soundcloud", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/fixtures/soundcloud.json b/tests/components/media_extractor/fixtures/soundcloud.json new file mode 100644 index 00000000000..ee430e43982 --- /dev/null +++ b/tests/components/media_extractor/fixtures/soundcloud.json @@ -0,0 +1,114 @@ +{ + "id": "223644255", + "uploader": "BRUTTOBAND", + "uploader_id": "111488150", + "uploader_url": "https://soundcloud.com/bruttoband", + "timestamp": 1442140228, + "title": "BRUTTO - \u0420\u043e\u0434\u043d\u044b \u043a\u0440\u0430\u0439", + "description": "", + "thumbnails": [ + { + "id": "mini", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-mini.jpg", + "width": 16, + "height": 16 + }, + { + "id": "tiny", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-tiny.jpg", + "width": 20, + "height": 20 + }, + { + "id": "small", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-small.jpg", + "width": 32, + "height": 32 + }, + { + "id": "badge", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-badge.jpg", + "width": 47, + "height": 47 + }, + { + "id": "t67x67", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t67x67.jpg", + "width": 67, + "height": 67 + }, + { + "id": "large", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-large.jpg", + "width": 100, + "height": 100 + }, + { + "id": "t300x300", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t300x300.jpg", + "width": 300, + "height": 300 + }, + { + "id": "crop", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-crop.jpg", + "width": 400, + "height": 400 + }, + { + "id": "t500x500", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t500x500.jpg", + "width": 500, + "height": 500 + }, + { + "id": "original", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-original.jpg", + "preference": 10 + } + ], + "duration": 229.089, + "webpage_url": "https://soundcloud.com/bruttoband/brutto-11", + "license": "all-rights-reserved", + "view_count": 291779, + "like_count": 3347, + "comment_count": 14, + "repost_count": 59, + "genre": "Brutto", + "formats": [ + { + "url": "https://cf-hls-media.sndcdn.com/playlist/50remGX1OqRY.128.mp3/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1tZWRpYS5zbmRjZG4uY29tL3BsYXlsaXN0LzUwcmVtR1gxT3FSWS4xMjgubXAzL3BsYXlsaXN0Lm0zdTgqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk1NTAzOTYzfX19XX0_&Signature=eNeIoSTgRZL89YBJYXpmRg0AVGk3M0gV4E4rYPYbFw6pTePHO4o8Mv6HwdK85FOMsaUHZvYgzc35uWPhAr1SUqqjnm--xwN8VUrDkCPgdv97Vrs9qJ9QElHKnlWhK2-BDs3Y7sDcAurA00L2uReB-vjI-4K65WBApYBTaUGnOACimoVAOWHmtigO0Ap5DxlEh7fqqwi88enEvVDE-98v5uX9FcV9lq9AfVwEtfqbPsjVJyh6WbWAB3PJDJElvV13RgKmzVvbFluLElYlDud9WMsHjztdWhdaRzGOj1AfcQcwkQbQlBRiAKMtqrRlzAAXnBfLvMF3DOvdYWeCwJeCXA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "mp3", + "abr": 128, + "format_id": "hls_mp3_128", + "protocol": "m3u8_native", + "preference": null, + "vcodec": "none" + }, + { + "url": "https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk1NTAzOTYzfX19XX0_&Signature=JAG~zJ~2NOWOgiuHLCSYWwdjUVuYWR2fBvmxPGSnLzMgX2xqu5~WfOk-gOyRUbHhnKnybUbP70cr6~t~Qx0KEU5mwIy2H0YhOXDHFX5RJVQlj1iCVuko-hAFJc7RtZuKTP5oCWOM-R2a6HfYN88YAIqgwWbGvTKin1CAgHaICeoM2p5O50n-kp05KgCw3RKcRutkYT-RVcWkmXtY4D4Jtw~LuBERDNyErseTHzmruDCkaYkVNlTcaIdgygQjgxVlgZiIRj-p0vRNO0qv5Bc0LfNMBzYm9fTAr86c~TzxyvQRhwHOPYp-DCXcs1K6i9x4BVvHWLOSHr0Dhd3X4fe5kw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "mp3", + "abr": 128, + "format_id": "http_mp3_128", + "protocol": "http", + "preference": null, + "vcodec": "none" + }, + { + "url": "https://cf-hls-opus-media.sndcdn.com/playlist/50remGX1OqRY.64.opus/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1vcHVzLW1lZGlhLnNuZGNkbi5jb20vcGxheWxpc3QvNTByZW1HWDFPcVJZLjY0Lm9wdXMvcGxheWxpc3QubTN1OCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2OTU1MDM5NjR9fX1dfQ__&Signature=HqyeKoK4gx1Bd3jId5v1-9ltDY2SN7fzGp6M7tW3kWluv8Ns0SgANPKG3~Tzo8hUrQzAYVvwMbO2F75b6NBeBr4xx0SRCeKotZdArzBFT4Wtrz4HtEddLyPjp12vWYCDuOshd1sTjrvaBAd9TAFTZuwyal1OKpLMsqK0QN-KFH-5GsmLDZTPWcoVkMqC7XBmNWTq0G1mtVeP57TN~9T7qEYqRLFIDXdm2HKSPSR4BB2gjaZUK22pBUPSGVr-ziBSpNVHvNISy2QdObjS5zjuAe8bl-npQ3PlFatYECDI3Gc~wjCNIJPdTpCbHUsX36SSR4dnKlgW1nYGx~eED7dppA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "opus", + "abr": 64, + "format_id": "hls_opus_64", + "protocol": "m3u8_native", + "preference": null, + "vcodec": "none" + } + ], + "original_url": "https://soundcloud.com/bruttoband/brutto-11", + "webpage_url_basename": "brutto-11", + "webpage_url_domain": "soundcloud.com", + "extractor": "soundcloud", + "extractor_key": "Soundcloud", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/fixtures/youtube_1.json b/tests/components/media_extractor/fixtures/youtube_1.json new file mode 100644 index 00000000000..e1283274f63 --- /dev/null +++ b/tests/components/media_extractor/fixtures/youtube_1.json @@ -0,0 +1,1430 @@ +{ + "id": "dQw4w9WgXcQ", + "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)", + "formats": [ + { + "asr": null, + "filesize": 80166145, + "format_id": "137", + "format_note": "1080p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 1080, + "quality": 9.0, + "has_drm": false, + "tbr": 3024.566, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=137&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=80166145&dur=212.040&lmt=1694045208995966&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAJvqkzrOZjbqQLPANU-Q0Ti57XZCS5MLEZMrme2Vqqj2AiEAoU5oDVbWI-82LxhSDuTtTvpgKEspgfrw7aPzQ8Di40w%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1920, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.640028", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 40874930, + "format_id": "248", + "format_note": "1080p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 1080, + "quality": 9.0, + "has_drm": false, + "tbr": 1542.159, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=248&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=40874930&dur=212.040&lmt=1694044655610179&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgM6ztX8BqXVEkyq4FTukRfb0mlWfDdll8wN_8iZvFoDMCIQDvRawrloLUqZWDjgf2ZZKkQPPX2NZQm5mUcIHjX04bWA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1920, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.40.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 16780212, + "format_id": "136", + "format_note": "720p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 633.096, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=136&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=16780212&dur=212.040&lmt=1694045071129751&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgG69VUvtxkC7_DuWzobsIDSBoAq9K8NfzCDI1BRkqC4ICIQD-G-4SOmZuQKmSkka0p8USe-GX_RzmuxsNPZj89r-9WA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1280, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D401F", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 15359727, + "format_id": "247", + "format_note": "720p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 579.502, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=247&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=15359727&dur=212.040&lmt=1694043486219683&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgclGPS3eSxjSMzpc-gOTA8Vsr4yfK6UCVyG5LUot-jMkCIQCdkrhr5s2GjZH5i8d_WciMXSN6kjqG9A6BCMzxqpeuRw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1280, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.31.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 8683274, + "format_id": "135", + "format_note": "480p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 480, + "quality": 7.0, + "has_drm": false, + "tbr": 327.608, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=135&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=8683274&dur=212.040&lmt=1694045045723793&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhANvnqVNT-trAFyACiWh8EllyhTzAuStHpLlDrTan7LxXAiEAy_Yajm6EEJUwcAVnEBRukcxc5-CB8UTY5BjB9oR1TeM%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 854, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 10927666, + "format_id": "244", + "format_note": "480p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 480, + "quality": 7.0, + "has_drm": false, + "tbr": 412.286, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=244&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=10927666&dur=212.040&lmt=1694043369037289&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAKBxnJIQLH7cWZFfZuMs5yhQ66jdt35KMdmqi5nmGIgnAiAzZ28nc8BNIKhKlhKBr5w6gWmvz-vm8E-PnNWigmhwgA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 854, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.30.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 5678772, + "format_id": "134", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 214.252, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=134&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=5678772&dur=212.040&lmt=1694045013473544&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAOHb4lBYlXkxFj1ZMXhNDcw1CQPWiB2c6Y6vOTGevdqJAiEAt644Dv84Eqzc6yfe1GG3sDMwYeLRUKA_KYHbSeJeKIo%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 640, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 6902164, + "format_id": "243", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 260.409, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=243&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=6902164&dur=212.040&lmt=1694043349554753&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAM3xj4hJ22Ur3eTOxA0LselI9THQg1Qb2gryxihUmPFLAiEAuQYROAwdEs6XdFszg8SRgCgojRUr1y9VS3096aQXnjc%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 640, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.21.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 3019976, + "format_id": "133", + "format_note": "240p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 240, + "quality": 5.0, + "has_drm": false, + "tbr": 113.939, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=133&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=3019976&dur=212.040&lmt=1694045014258984&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgWmsCuyZEqz2NashfdLp92iJqaqRtA8bYJJhohjGFxzgCIQD3aQZ90zKGHu-JXiJZMViWuCb0UeZ-MesxOGi_gMWHxA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 426, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D4015", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 4021173, + "format_id": "242", + "format_note": "240p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 240, + "quality": 5.0, + "has_drm": false, + "tbr": 151.713, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=242&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=4021173&dur=212.040&lmt=1694043379783601&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgAVwANVM5s1vtgkPKId_2b9bw2d_Lhbvkvm2J2OJM-fUCIQDwcC5FLMxOF3g6nZq1vpf0d7dyKnp0plE1Niy3rZ6Cdg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 426, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.20.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 1863601, + "format_id": "160", + "format_note": "144p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 70.311, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=160&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=1863601&dur=212.040&lmt=1694045032286738&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAL_zAjcV7CL3hke-z49D6nQ7k5dCTVweXQdj4_cVHIc2AiB9bkIVgy7GYGFUGo36PYjnlN_8KNnyxiNhh0M76Fjjgw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D400C", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 2404581, + "format_id": "278", + "format_note": "144p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 90.721, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=278&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=2404581&dur=212.040&lmt=1694043382822868&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAOetHAZolpx87k21SDKePQP6gZHC3CWiQ_DtEQd1bDRvAiEA8GGA-2C5lIFuucuPqdnS4FZiGdKYgWUTlJ-9yQEnSR0%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.11.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 22050, + "filesize": 1294944, + "format_id": "139", + "format_note": "low", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 2.0, + "has_drm": false, + "tbr": 48.823, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=139&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=audio%2Fmp4&gir=yes&clen=1294944&dur=212.183&lmt=1694042119353699&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAMKE8c3kivZrqSOCOcLzUCa1erqAaOj6K7SWFAcCJyCXAiBOFkaL_lvsXhZeLwyOUP98LBTGxUHEurO_IWZOeRCkAQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "m4a", + "vcodec": "none", + "acodec": "mp4a.40.5", + "dynamic_range": null, + "container": "m4a_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 44100, + "filesize": 3433514, + "format_id": "140", + "format_note": "medium", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 3.0, + "has_drm": false, + "tbr": 129.51, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=140&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=audio%2Fmp4&gir=yes&clen=3433514&dur=212.091&lmt=1694042124987733&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAKw2jXdoZhn8sjUu-1MSxfV1p0TzRyqjuooRwoQohtOwAiANfeuxDTlTpi_f9scAC0n01xOejhRLD0m-6pl3oo7wIQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "m4a", + "vcodec": "none", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "container": "m4a_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 22050, + "filesize": 2086732, + "format_id": "17", + "format_note": "144p", + "source_preference": -1, + "fps": 6, + "audio_channels": 1, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 78.693, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=17&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2F3gpp&gir=yes&clen=2086732&dur=212.137&lmt=1694042486266781&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAIOMPqxbtJwYRIrAYmr0I9rEovBipWNTTg9AMju1ehECAiEA7vjnz-TCwh2zQQm4vmXW0nGpft4nX42Ql_hwHHCA-Yk%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 176, + "language": "en", + "language_preference": -1, + "preference": -2, + "ext": "3gp", + "vcodec": "mp4v.20.3", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 44100, + "filesize": null, + "format_id": "18", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": 2, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 343.32, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=18&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045104514388&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4538434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgFhTg7TavCE2HBWS9I3agqj3CG2RqrvxLJt6JgHtN4O4CIF-IDHhEPLlkGP2QtwJ19sSumUVPqVElVXjrM-qqYIae&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 640, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.42001E", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 44100, + "filesize": null, + "format_id": "22", + "format_note": "720p", + "source_preference": -5, + "fps": 25, + "audio_channels": 2, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 762.182, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgHVwajP0J2fiJ1ERoAonpxghXGzDmEXh3rvJ399UEMWECIFdBjiVUOk7QdiFBxQ4QqojJd8p_PfL25TV_8TBrp_Kb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 1280, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.64001F", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 31265835, + "format_id": "399", + "format_note": "1080p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 1080, + "quality": 9.0, + "has_drm": false, + "tbr": 1179.62, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=399&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=31265835&dur=212.040&lmt=1694042163788395&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAMbD0sObTPrf1j0GESI-SRztzhMi98xn1XBMfFsnMjLFAiEAnMCImljVChi4G_wjA9UE2EN9xQHJ7LhuEO9HeNlR334%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 1920, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.08M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 17466721, + "format_id": "398", + "format_note": "720p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 658.997, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=398&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=17466721&dur=212.040&lmt=1694042319819525&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgfJ1sWUmxG82ls65giBWKTwsH7UP4ItT0soOPZSEtKg4CIQC_GFYhkfiktXrWOoKWW2j50GkQX7dE7mfWzvjh-XnIXA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 1280, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.05M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 9434981, + "format_id": "397", + "format_note": "480p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 480, + "quality": 7.0, + "has_drm": false, + "tbr": 355.969, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=397&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=9434981&dur=212.040&lmt=1694042458043976&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgFFbe3pmUWAFqJCycmW7hGbeJuC8dfEax2p6v9J-y9GQCIB-2d1ss2yBL3yhesngue7dM5AsJqLNOMLnCD51o-1zW&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 854, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.04M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 5438397, + "format_id": "396", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 205.183, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=396&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=5438397&dur=212.040&lmt=1694042190822892&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgNr6BZ03kG8p2KTAv7gOZ01pEcCWwnkWvjOdxmGn1X4ACICrqnbbGqLvm0jpEqXYOXMISHcPt7vQVzwohM84tfeYb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 640, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.01M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 3024455, + "format_id": "395", + "format_note": "240p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 240, + "quality": 5.0, + "has_drm": false, + "tbr": 114.108, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=395&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=3024455&dur=212.040&lmt=1694042297309821&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAM7Up-h9A3SMwIYMo6V5t4oM7BpkjnEIcO_s7BTR1hfzAiA72-vEcn4y21NtpQkzpTZI_BdjCeCUez43ohuzw4MJsA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 426, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.00M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 1416915, + "format_id": "394", + "format_note": "144p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 53.458, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=394&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=1416915&dur=212.040&lmt=1694042192787352&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgLYhy9j4feWSuTyTnJr2MF4xiEpALLDeez2_BwF__Qq8CIQDBTg9R-8YOcUtA4-R-Gu8A7o_66wGf69Vky62ZE-T0Zw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.00M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 847252, + "format_id": "597", + "format_note": "144p", + "source_preference": -1, + "fps": 13, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 31.959, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=597&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=847252&dur=212.080&lmt=1694042194934376&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgH6Tk52MhzNtfm-6d1XHdQfIh12aqbEohhH-ffBZP9z0CIQDJwPFA7eTz6LdcZaBlfnlogft7pgtrXHvm6DIHWCODUg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4d400b", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 643306, + "format_id": "598", + "format_note": "144p", + "source_preference": -1, + "fps": 13, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 24.266, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=598&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=643306&dur=212.080&lmt=1694042224218554&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgHwZWhUJuuFMMAva_Katkrgk3FGNcBlHCwBVwV1jGz4ACIQCerrScqjke9mtPVPwYZraaCp4u7VkFz1hIzx-Fl_7HzQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp9", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 1232413, + "format_id": "249", + "format_note": "low", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 2.0, + "has_drm": false, + "tbr": 46.492, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=249&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=1232413&dur=212.061&lmt=1694040798737498&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgD3BhwkfqYjk1FEud7AdjzD9RJImYWaebeN9Ip7HuX3ICIQDCzT7tMYmDyb_fz4TB4GPwroXqO55NV5h7Ao-IoPq0Kw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 1630086, + "format_id": "250", + "format_note": "low", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 2.0, + "has_drm": false, + "tbr": 61.494, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=250&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=1630086&dur=212.061&lmt=1694040798724510&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgJ1nALE0kFCC4pg2mj0LpB_ZwivihtQo6ugYw-AzKJAsCIQD5q8PFpJtloWUmuK2A80NC7c2hr_9OUldFCXOCLyPN3Q%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 3437753, + "format_id": "251", + "format_note": "medium", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 3.0, + "has_drm": false, + "tbr": 129.689, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=251&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=3437753&dur=212.061&lmt=1694040798752663&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAPSOofxlLab49bVcmvVP8wmIHVWvqDyOd11oJdP1RPFfAiAOCKp1VodP12z6FqdWxp_2xYcS2J949BbgFWqlfGkHjA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 22050, + "filesize": 817805, + "format_id": "599", + "format_note": "ultralow", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 1.0, + "has_drm": false, + "tbr": 30.833, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=599&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fmp4&gir=yes&clen=817805&dur=212.183&lmt=1694040788792847&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAKEpb371lM9F_wlPf3i5D6zreL34as0UmzOJxw5TXqlrAiEAjNU124xhGmkotlkRSCWdF15IBB-frezyqNQY1AV-l4M%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "m4a", + "vcodec": "none", + "acodec": "mp4a.40.5", + "dynamic_range": null, + "container": "m4a_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 832823, + "format_id": "600", + "format_note": "ultralow", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 1.0, + "has_drm": false, + "tbr": 31.418, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=600&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=832823&dur=212.061&lmt=1694040798740210&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgRqdkURprOblK7GurvCvDxSunECdm96nzQwzwHuDvUKcCIQCmeMMeDP816FMH0GgpugoQhe4z4X6nDYoY3PtQhShtWA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "format_id": "233", + "format_note": "Default", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/233/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/goi/133/sgoap/clen%3D1294944%3Bdur%3D212.183%3Bgir%3Dyes%3Bitag%3D139%3Blmt%3D1694042119353699/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,goi,sgoap,vprv,playlist_type/sig/AOq0QJ8wRgIhANECzBL2bCCpseaXL3qapc_gEQkpP-2eTqyPCspG44PXAiEA4J6i9mS_2vJB4TtIwHjg7-f-KCyiYSs-kL5dcEkToRg%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRAIgUS67qZ67NkB4J8491hAMcCKs400sACGWUhNcrXWefx4CIB64Ny0g5mTEFnTryntu2vexyLjfXtNHmvEAYgosB_9w/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "language": "en", + "ext": "mp4", + "protocol": "m3u8_native", + "preference": null, + "quality": -1, + "has_drm": false, + "vcodec": "none", + "source_preference": -1 + }, + { + "format_id": "234", + "format_note": "Default", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/234/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/goi/133/sgoap/clen%3D3433514%3Bdur%3D212.091%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1694042124987733/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,goi,sgoap,vprv,playlist_type/sig/AOq0QJ8wRgIhAM_Q8sGDeFsxPWBEUPdlx6Mul7XMV1uCmH_5jdrqR06cAiEAqpNga8OlFdW_uqNwYxIL70Ki64lw0hKT700Z8dZPTtA%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgM8VqbzvtcXd5PhUv9cffclj4q86NQYiKEJw9CuwxlbYCIQDntLRgkHqq_HrUvzSiwWLph8lvrnAgSps0aAilpdDKfg%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "language": "en", + "ext": "mp4", + "protocol": "m3u8_native", + "preference": null, + "quality": -1, + "has_drm": false, + "vcodec": "none", + "source_preference": -1 + }, + { + "format_id": "229", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/229/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D3019976%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D133%3Blmt%3D1694045014258984/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIgDXYLvO8hsKnxOFI4QK0KR6_bH3y963ahqlBfJqHHOBgCIQDlYY2nTWarn59nFcVOD35IVk5obSssFUeidm3u4n6FBw%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAKnlpIqSLjxTjoejuijzzpuB-Di1I2eiDdzTDDWNboffAiBbdWGc04XUqoe4imJg9kVp3fWTDOFXFVnhAXfqp3Qp6w%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 225.675, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 5, + "has_drm": false, + "width": 426, + "height": 240, + "vcodec": "avc1.4D4015", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "230", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/230/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D5678772%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D134%3Blmt%3D1694045013473544/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRgIhAIQvJojwVvM5frF6Na5JJ5zkd1VdUfPLdqPW5Tht2eNkAiEA3437jLFLon8Rpbsp5krc66qddvGP4rj8sbwJd4rlHmU%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAIL_XGGyZVNILzOXnviLoYEMYJfAngPM1eBZGgN6wEr-AiAInhTuU6hkWCkpZGr9dPeXYSfa3brLjZNivRNbpJB8QA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 478.155, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 6, + "has_drm": false, + "width": 640, + "height": 360, + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "231", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/231/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D8683274%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D135%3Blmt%3D1694045045723793/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRAIgdgnN5tIO26lbTUty02k4r8k9-oaX_7m4LsLXMRBE7n8CIEb2DJRXZ0dGH_ZDbtYAapQKCiCxqht8Bznh0LmS83cw/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgfNxyHiNcbQeIRKSwnBRTymUYFDqdhNZqEdRJ5-dl_uICIQD6XyM3ReaIZkg4DsK6ys3VjdMroKJUBHPx4pZGYbqJ0Q%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 660.067, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 7, + "has_drm": false, + "width": 854, + "height": 480, + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "232", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/232/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D16780212%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D136%3Blmt%3D1694045071129751/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRgIhAKklfVW5UAGwbMKG5dyOkjEW9RqlFeZXuS9RKazS7hgHAiEAluSFc2bFqy_0nb32n7mR-SOR0gCmAdFwl35gbDldf3w%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAOqcypFYB4AyrOahc7gihR0-jqv-Gzc8JHdRtQEn3r9wAiEA2cqI-R7Cjr-UaDu-B9miweYpBXWzDeC8PoxK_0bkm5c%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 1130.986, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 8, + "has_drm": false, + "width": 1280, + "height": 720, + "vcodec": "avc1.4D401F", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "269", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/269/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D1863601%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D160%3Blmt%3D1694045032286738/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAOI4zFdpx6CuAJUVfQO8mSPKS-WskVy5lco9PRAL-TfDAiABtG5PX_rqg5Vr77L9IKeZgKU4Mbt-YLWmvxQos9prAg%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAJA2-IQoPA4HYhx5ehZh9_b91jlS-QLvYO8xOp8HXN2uAiAUYQXpYWShcC4WGSKU0_MMNwdKqeQgdYPtqIXTVTKZuA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 156.229, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 0, + "has_drm": false, + "width": 256, + "height": 144, + "vcodec": "avc1.4D400C", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "270", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/270/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D80166145%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D137%3Blmt%3D1694045208995966/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRAIgbzf0MRvdmSh-0Na1BR8xckB4DoMcL2nNJl3vDRew0AICIANhvJC9Q1hAqDjjLubtM7DoNaY-PtJpVlbfaL81F3l8/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAOya7lZyTdnNmoyCacVPgOcsyLDSKevmW3xFt_afVsWfAiB-hASkkk9GrfTuT-6adP2aXYrMXkiB-Y8nuX-wrWUmSg%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 4901.412, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 9, + "has_drm": false, + "width": 1920, + "height": 1080, + "vcodec": "avc1.640028", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "602", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/602/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D643306%3Bdur%3D212.080%3Bgir%3Dyes%3Bitag%3D598%3Blmt%3D1694042224218554/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAJ5BHwzV7tAkb9pRDEsdmzFTJOrsuq-IZSmBaa8ZgWU4AiBoqSh-knrE3feDNHwFm_0fAM_qNFn3xvV98kmX_-pYPA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAIQe5bcuBu9He-VtMYGRHkjZuDoUvmnuIbyjxf6sncbKAiEA9iegdULdUppfIh2N3Lz4Kt0PwtdV-c5G1gRDaO-U7t0%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 80.559, + "ext": "mp4", + "fps": 13.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 0, + "has_drm": false, + "width": 256, + "height": 144, + "vcodec": "vp09.00.10.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "603", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/603/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D2404581%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D278%3Blmt%3D1694043382822868/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRAIgW4kL5-Bqq0OV_FyB5Df0QcqkyUTYid2eN4BUzn8sp98CIFqLxBBSz7H3PaXJ4NycNae2P0--5ri0HHMItBr8PKIP/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAJ6-KuK2swgLKLyCSnGkgsoVy2VR9SuNpx6Crrz9mc9GAiEAqbUS5dWqCZkA7oSKAONrBYKbsjgiXwT1EV6Uxj8ToOU%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 153.593, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 0, + "has_drm": false, + "width": 256, + "height": 144, + "vcodec": "vp09.00.11.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "604", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/604/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D4021173%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D242%3Blmt%3D1694043379783601/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAIW_PHJodC3iY33S6s7ju5X9_6oByqQFda5vPWR8jwrgAiB_csQrznhta4iTLmj6Xzybwgfe5CRA6TFV1KbQ21QJZw%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgHGms27SWPkQSOt2slvmBWboDwV_BrqW_RoRlpdqD5rACIQCVpBzzlQxE44nHEJ4hoYD2QUvIm732saxlZ2fLjfljJQ%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 287.523, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 5, + "has_drm": false, + "width": 426, + "height": 240, + "vcodec": "vp09.00.20.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "605", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/605/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D6902164%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D243%3Blmt%3D1694043349554753/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIgJd3aB7R2t5a0XNGTqDIuYSimhFpK2hEvDD1-itRftKkCIQCqe4F0OhI5PSp0tSYEXlngrmJgfTGIuVZUMH8saPZlnQ%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRAIgQveRyDrh7DOwnJgI7dzbB3XLvqrPvKwutQI7ZjCtIs0CIHRxPzpMlfC9QmQMTu2SIGs7QP8bP1Nn65JxYGRecFCt/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 566.25, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 6, + "has_drm": false, + "width": 640, + "height": 360, + "vcodec": "vp09.00.21.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "606", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/606/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D10927666%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D244%3Blmt%3D1694043369037289/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAIIZhHj_PxMIRxj2AvOoouUWwZPnPs3-autC7-_Qu1dnAiAtQJp9ZV1TVQXd02g1viHWghB6tKSD5_jcRHzLPHIAeA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgXJh3M-PAlfv0g5H_brRmCBl1Z0w0b5y9mqIdEsZSp-0CIQD4j4piciikRuQI3KX-HFizmq-dPxMc-aqVBFYw43-NRA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 733.359, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 7, + "has_drm": false, + "width": 854, + "height": 480, + "vcodec": "vp09.00.30.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "609", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/609/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D15359727%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D247%3Blmt%3D1694043486219683/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhANJRh709Wuh23a9Wj2UUKFE9qjHRMscBHd3fQjuNjK5zAiB4gh40D5HmwOx3JuqptUi44o5EtkdzK0IQEunFmwOPiA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgRcLcLuy1kw90oGfrplalJdXQ4t9tjEQNH-bp7lGNsCICIQDLlCQYGjyHjnkZONzlaYidWV7-_stKKzzkhz3xEsOP4w%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 1179.472, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 8, + "has_drm": false, + "width": 1280, + "height": 720, + "vcodec": "vp09.00.31.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "614", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/614/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D40874930%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D248%3Blmt%3D1694044655610179/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIga8rGnkxIK7d-77rdKN1yHtRP9NyUJGXfRyVba5rKVRoCIQD7mJ1LOowgdfuJQuXTvarIbd54VwB6hM5O05zpPdFJDQ%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRAIgGwgwIro32WyMRnb_Ccp6z_iH1YZLIwF2D8nnhQOoyJwCIGvOkZvz50XkJPrLReF_rHyHcsgE9PM_hcpudysB6YN9/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 2831.123, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 9, + "has_drm": false, + "width": 1920, + "height": 1080, + "vcodec": "vp09.00.40.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "616", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/616/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D99471214%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D356%3Blmt%3D1694043438471036/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAPTG-DKQd4Rtv1dvvExqPNGfPU_wBRbsGSIYRqJ3UCDEAiBBYBgPR_gAJAiCr2eHvR3hu6uWUEUCvEN5pr5Dm2_5gA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgIzhyfPERJMLLzgSld4XG3lYTJKhsmpOrVD2v_siZfEgCIQCPOKf2Or4aqJhe--d_2Qh_ljI39BS6JH7x6BPXC7f_NA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 5704.254, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 9, + "has_drm": false, + "width": 1920, + "height": 1080, + "vcodec": "vp09.00.40.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": 99, + "format_note": "Premium" + }, + { + "format_id": "sb0", + "format_note": "storyboard", + "ext": "mhtml", + "protocol": "mhtml", + "acodec": "none", + "vcodec": "none", + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "width": 160, + "height": 90, + "fps": 0.5094339622641509, + "rows": 5, + "columns": 5, + "fragments": [ + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M0.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M1.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M2.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M3.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M4.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 15.703703703703724 + } + ] + }, + { + "format_id": "sb1", + "format_note": "storyboard", + "ext": "mhtml", + "protocol": "mhtml", + "acodec": "none", + "vcodec": "none", + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L1/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCTXAc1RH5-ZIGm3FlRiYRUkzQXug", + "width": 80, + "height": 45, + "fps": 0.5094339622641509, + "rows": 10, + "columns": 10, + "fragments": [ + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L1/M0.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCTXAc1RH5-ZIGm3FlRiYRUkzQXug", + "duration": 196.29629629629628 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L1/M1.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCTXAc1RH5-ZIGm3FlRiYRUkzQXug", + "duration": 15.703703703703724 + } + ] + }, + { + "format_id": "sb2", + "format_note": "storyboard", + "ext": "mhtml", + "protocol": "mhtml", + "acodec": "none", + "vcodec": "none", + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCSw4ypjBBVyVfNU-jl-4aLZArqkA", + "width": 48, + "height": 27, + "fps": 0.4716981132075472, + "rows": 10, + "columns": 10, + "fragments": [ + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCSw4ypjBBVyVfNU-jl-4aLZArqkA", + "duration": 212.0 + } + ] + } + ], + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg", + "height": 180, + "width": 320, + "preference": -11 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", + "height": 360, + "width": 480, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sddefault.jpg", + "height": 480, + "width": 640, + "preference": -5 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/default.webp", + "height": 90, + "width": 120, + "preference": -12 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mqdefault.webp", + "height": 180, + "width": 320, + "preference": -10 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hqdefault.webp", + "height": 360, + "width": 480, + "preference": -6 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", + "height": 480, + "width": 640, + "preference": -4 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDd2KtelLHaNSXrI9_5K-NvTscKNw", + "height": 94, + "width": 168, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBUpEOOWUXWkNyijQuZ4UPzp2BE-w", + "height": 110, + "width": 196, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBCyhr8AqpJ1SxKVU6SyK5ODJ_IpA", + "height": 138, + "width": 246, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB_p0PncTtkrhaNDZtntrE3gKkoYw", + "height": 188, + "width": 336, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/maxresdefault.webp", + "height": 1080, + "width": 1920, + "preference": 0 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg", + "height": 720, + "width": 1280, + "preference": -1 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq720.webp", + "preference": -2 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq720.jpg", + "preference": -3 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/0.webp", + "preference": -8 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/0.jpg", + "preference": -9 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/default.jpg", + "preference": -13 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sd1.webp", + "preference": -14 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sd1.jpg", + "preference": -15 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sd2.webp", + "preference": -16 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sd2.jpg", + "preference": -17 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sd3.webp", + "preference": -18 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sd3.jpg", + "preference": -19 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq1.webp", + "preference": -20 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq1.jpg", + "preference": -21 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq2.webp", + "preference": -22 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq2.jpg", + "preference": -23 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq3.webp", + "preference": -24 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq3.jpg", + "preference": -25 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mq1.webp", + "preference": -26 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mq1.jpg", + "preference": -27 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mq2.webp", + "preference": -28 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mq2.jpg", + "preference": -29 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mq3.webp", + "preference": -30 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mq3.jpg", + "preference": -31 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/1.webp", + "preference": -32 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/1.jpg", + "preference": -33 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/2.webp", + "preference": -34 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/2.jpg", + "preference": -35 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/3.webp", + "preference": -36 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/3.jpg", + "preference": -37 + } + ], + "thumbnail": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/maxresdefault.webp", + "description": "The official video for \u201cNever Gonna Give You Up\u201d by Rick Astley\n\n\u2018Hold Me In Your Arms\u2019 \u2013 deluxe blue vinyl, 2CD and digital deluxe out 12th May 2023 Pre-order here \u2013 https://rick-astley.lnk.to/HMIYA2023ID\n\n\u201cNever Gonna Give You Up\u201d was a global smash on its release in July 1987, topping the charts in 25 countries including Rick\u2019s native UK and the US Billboard Hot 100. It also won the Brit Award for Best single in 1988. Stock Aitken and Waterman wrote and produced the track which was the lead-off single and lead track from Rick\u2019s debut LP \u201cWhenever You Need Somebody\u201d. The album was itself a UK number one and would go on to sell over 15 million copies worldwide.\n\nThe legendary video was directed by Simon West \u2013 who later went on to make Hollywood blockbusters such as Con Air, Lara Croft \u2013 Tomb Raider and The Expendables 2. The video passed the 1bn YouTube views milestone on 28 July 2021.\n\nSubscribe to the official Rick Astley YouTube channel: https://RickAstley.lnk.to/YTSubID\n\nFollow Rick Astley:\nFacebook: https://RickAstley.lnk.to/FBFollowID \nTwitter: https://RickAstley.lnk.to/TwitterID \nInstagram: https://RickAstley.lnk.to/InstagramID \nWebsite: https://RickAstley.lnk.to/storeID \nTikTok: https://RickAstley.lnk.to/TikTokID\n\nListen to Rick Astley:\nSpotify: https://RickAstley.lnk.to/SpotifyID \nApple Music: https://RickAstley.lnk.to/AppleMusicID \nAmazon Music: https://RickAstley.lnk.to/AmazonMusicID \nDeezer: https://RickAstley.lnk.to/DeezerID \n\nLyrics:\nWe\u2019re no strangers to love\nYou know the rules and so do I\nA full commitment\u2019s what I\u2019m thinking of\nYou wouldn\u2019t get this from any other guy\n\nI just wanna tell you how I\u2019m feeling\nGotta make you understand\n\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\nWe\u2019ve known each other for so long\nYour heart\u2019s been aching but you\u2019re too shy to say it\nInside we both know what\u2019s been going on\nWe know the game and we\u2019re gonna play it\n\nAnd if you ask me how I\u2019m feeling\nDon\u2019t tell me you\u2019re too blind to see\n\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\n#RickAstley #NeverGonnaGiveYouUp #WheneverYouNeedSomebody #OfficialMusicVideo", + "channel_id": "UCuAXFkgsw1L7xaCfnd5JJOw", + "channel_url": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + "duration": 212, + "view_count": 1450567564, + "average_rating": null, + "age_limit": 0, + "webpage_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "categories": ["Music"], + "tags": [ + "rick astley", + "Never Gonna Give You Up", + "nggyu", + "never gonna give you up lyrics", + "rick rolled", + "Rick Roll", + "rick astley official", + "rickrolled", + "Fortnite song", + "Fortnite event", + "Fortnite dance", + "fortnite never gonna give you up", + "rick roll", + "rickrolling", + "rick rolling", + "never gonna give you up", + "80s music", + "rick astley new", + "animated video", + "rickroll", + "meme songs", + "never gonna give u up lyrics", + "Rick Astley 2022", + "never gonna let you down", + "animated", + "rick rolls 2022", + "never gonna give you up karaoke" + ], + "playable_in_embed": true, + "live_status": "not_live", + "release_timestamp": null, + "_format_sort_fields": [ + "quality", + "res", + "fps", + "hdr:12", + "source", + "vcodec:vp9.2", + "channels", + "acodec", + "lang", + "proto" + ], + "automatic_captions": {}, + "subtitles": {}, + "comment_count": 2300000, + "chapters": null, + "heatmap": [], + "like_count": 16869622, + "channel": "Rick Astley", + "channel_follower_count": 3890000, + "channel_is_verified": true, + "uploader": "Rick Astley", + "uploader_id": "@RickAstleyYT", + "uploader_url": "https://www.youtube.com/@RickAstleyYT", + "upload_date": "20091025", + "availability": "public", + "__post_extractor": null, + "original_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "webpage_url_basename": "watch", + "webpage_url_domain": "youtube.com", + "extractor": "youtube", + "extractor_key": "Youtube" +} diff --git a/tests/components/media_extractor/fixtures/youtube_empty_playlist.json b/tests/components/media_extractor/fixtures/youtube_empty_playlist.json new file mode 100644 index 00000000000..37f22693528 --- /dev/null +++ b/tests/components/media_extractor/fixtures/youtube_empty_playlist.json @@ -0,0 +1,49 @@ +{ + "id": "PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO", + "title": "Very important videos", + "availability": "public", + "channel_follower_count": null, + "description": "Not original", + "tags": [], + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLDBCH5IQ0obogxXhAzIH8pE0d7r1Q", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLAybhgn-CoPMjBE-0VfBDqvy0jyOQ", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLDutIdjr5zTE9G78eWf83-mGXYnUA", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLD2884fHuvAv8ysHA48LD3uArB6bA", + "height": 188, + "width": 336 + } + ], + "modified_date": "20230813", + "view_count": 5770834, + "playlist_count": 3, + "channel": "ZulTarx", + "channel_id": "UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_id": "@Armand314", + "uploader": "ZulTarx", + "channel_url": "https://www.youtube.com/channel/UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_url": "https://www.youtube.com/@Armand314", + "_type": "playlist", + "entries": [], + "extractor_key": "YoutubeTab", + "extractor": "youtube:tab", + "webpage_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO", + "original_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO", + "webpage_url_basename": "playlist", + "webpage_url_domain": "youtube.com", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/fixtures/youtube_playlist.json b/tests/components/media_extractor/fixtures/youtube_playlist.json new file mode 100644 index 00000000000..053b243a1be --- /dev/null +++ b/tests/components/media_extractor/fixtures/youtube_playlist.json @@ -0,0 +1,179 @@ +{ + "id": "PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP", + "title": "Very important videos", + "availability": "public", + "channel_follower_count": null, + "description": "Not original", + "tags": [], + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLDBCH5IQ0obogxXhAzIH8pE0d7r1Q", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLAybhgn-CoPMjBE-0VfBDqvy0jyOQ", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLDutIdjr5zTE9G78eWf83-mGXYnUA", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLD2884fHuvAv8ysHA48LD3uArB6bA", + "height": 188, + "width": 336 + } + ], + "modified_date": "20230813", + "view_count": 5770834, + "playlist_count": 3, + "channel": "ZulTarx", + "channel_id": "UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_id": "@Armand314", + "uploader": "ZulTarx", + "channel_url": "https://www.youtube.com/channel/UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_url": "https://www.youtube.com/@Armand314", + "_type": "playlist", + "entries": [ + { + "_type": "url", + "ie_key": "Youtube", + "id": "q6EoRBvdVPQ", + "url": "https://www.youtube.com/watch?v=q6EoRBvdVPQ", + "title": "Yee", + "description": null, + "duration": 10, + "channel_id": "UC-fD_qwTEQQ1L-MUWx_mNvg", + "channel": "revergo", + "channel_url": "https://www.youtube.com/channel/UC-fD_qwTEQQ1L-MUWx_mNvg", + "uploader": "revergo", + "uploader_id": "@revergo", + "uploader_url": "https://www.youtube.com/@revergo", + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC6AKKAgwIABABGDwgZSg_MA8=&rs=AOn4CLAJYg16HMBdEsv9lYBJyNqA5G3anQ", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC6AKKAgwIABABGDwgZSg_MA8=&rs=AOn4CLAgCNP9UuQas-D59hHHM-RqkUvA6g", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLCTWaY5897XxhcpRyVtGQQNuMHfTg", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLCeS6NC75yTYvyP4DsehZ3oXNuxMQ", + "height": 188, + "width": 336 + } + ], + "timestamp": null, + "release_timestamp": null, + "availability": null, + "view_count": 96000000, + "live_status": null, + "channel_is_verified": null + }, + { + "_type": "url", + "ie_key": "Youtube", + "id": "8YWl7tDGUPA", + "url": "https://www.youtube.com/watch?v=8YWl7tDGUPA", + "title": "color red", + "description": null, + "duration": 17, + "channel_id": "UCbYMTn6xKV0IKshL4pRCV3g", + "channel": "Alex Jimenez", + "channel_url": "https://www.youtube.com/channel/UCbYMTn6xKV0IKshL4pRCV3g", + "uploader": "Alex Jimenez", + "uploader_id": "@alexjimenez1237", + "uploader_url": "https://www.youtube.com/@alexjimenez1237", + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AG2BIACwAKKAgwIABABGGUgXShUMA8=&rs=AOn4CLBqzngIx-4i_HFvqloetUfeN8yrYw", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AG2BIACwAKKAgwIABABGGUgXShUMA8=&rs=AOn4CLB7mWPQmdL2QBLxTHhrgbFj2jFaCg", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgBtgSAAsACigIMCAAQARhlIF0oVDAP&rs=AOn4CLA9YAIO3g_DnClsuc5LjMQn4O9ZQQ", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgBtgSAAsACigIMCAAQARhlIF0oVDAP&rs=AOn4CLDPHY6aG08hlTJMlc-LJt9ywtpWEg", + "height": 188, + "width": 336 + } + ], + "timestamp": null, + "release_timestamp": null, + "availability": null, + "view_count": 30000000, + "live_status": null, + "channel_is_verified": null + }, + { + "_type": "url", + "ie_key": "Youtube", + "id": "6bnanI9jXps", + "url": "https://www.youtube.com/watch?v=6bnanI9jXps", + "title": "Terrible Mall Commercial", + "description": null, + "duration": 31, + "channel_id": "UCLmnB20wsih9F5N0o5K0tig", + "channel": "quantim", + "channel_url": "https://www.youtube.com/channel/UCLmnB20wsih9F5N0o5K0tig", + "uploader": "quantim", + "uploader_id": "@Potatoflesh", + "uploader_url": "https://www.youtube.com/@Potatoflesh", + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAsyI0ZJA9STG8vlSdRkKk55ls5Dg", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD2bZ9S8AB4UGsZlx_8TjBoL72enA", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCKNlgvl_7lKoFq8vyDYZRtTs4woA", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBeZv8F8IyICmKD9qjo9pTMJmM8ug", + "height": 188, + "width": 336 + } + ], + "timestamp": null, + "release_timestamp": null, + "availability": null, + "view_count": 26000000, + "live_status": null, + "channel_is_verified": null + } + ], + "extractor_key": "YoutubeTab", + "extractor": "youtube:tab", + "webpage_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP", + "original_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP", + "webpage_url_basename": "playlist", + "webpage_url_domain": "youtube.com", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/snapshots/test_init.ambr b/tests/components/media_extractor/snapshots/test_init.ambr index d70c370b60c..ed56f40af73 100644 --- a/tests/components/media_extractor/snapshots/test_init.ambr +++ b/tests/components/media_extractor/snapshots/test_init.ambr @@ -1,4 +1,24 @@ # serializer version: 1 +# name: test_extract_media_service[https://soundcloud.com/bruttoband/brutto-11] + dict({ + 'url': 'https://cf-hls-opus-media.sndcdn.com/playlist/50remGX1OqRY.64.opus/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1vcHVzLW1lZGlhLnNuZGNkbi5jb20vcGxheWxpc3QvNTByZW1HWDFPcVJZLjY0Lm9wdXMvcGxheWxpc3QubTN1OCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2OTU1MDM5NjR9fX1dfQ__&Signature=HqyeKoK4gx1Bd3jId5v1-9ltDY2SN7fzGp6M7tW3kWluv8Ns0SgANPKG3~Tzo8hUrQzAYVvwMbO2F75b6NBeBr4xx0SRCeKotZdArzBFT4Wtrz4HtEddLyPjp12vWYCDuOshd1sTjrvaBAd9TAFTZuwyal1OKpLMsqK0QN-KFH-5GsmLDZTPWcoVkMqC7XBmNWTq0G1mtVeP57TN~9T7qEYqRLFIDXdm2HKSPSR4BB2gjaZUK22pBUPSGVr-ziBSpNVHvNISy2QdObjS5zjuAe8bl-npQ3PlFatYECDI3Gc~wjCNIJPdTpCbHUsX36SSR4dnKlgW1nYGx~eED7dppA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ', + }) +# --- +# name: test_extract_media_service[https://test.com/abc] + dict({ + 'url': 'https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ', + }) +# --- +# name: test_extract_media_service[https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP] + dict({ + 'url': 'https://www.youtube.com/watch?v=q6EoRBvdVPQ', + }) +# --- +# name: test_extract_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ] + dict({ + 'url': 'https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgHVwajP0J2fiJ1ERoAonpxghXGzDmEXh3rvJ399UEMWECIFdBjiVUOk7QdiFBxQ4QqojJd8p_PfL25TV_8TBrp_Kb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D', + }) +# --- # name: test_no_target_entity ReadOnlyDict({ 'device_id': list([ diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index e47f0ae1470..388ea3be1fd 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -9,9 +9,14 @@ import pytest from syrupy import SnapshotAssertion from yt_dlp import DownloadError -from homeassistant.components.media_extractor import DOMAIN +from homeassistant.components.media_extractor.const import ( + ATTR_URL, + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, +) from homeassistant.components.media_player import SERVICE_PLAY_MEDIA from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from tests.common import load_json_object_fixture @@ -30,6 +35,58 @@ async def test_play_media_service_is_registered(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.services.has_service(DOMAIN, SERVICE_PLAY_MEDIA) + assert hass.services.has_service(DOMAIN, SERVICE_EXTRACT_MEDIA_URL) + + +@pytest.mark.parametrize( + "url", + [ + YOUTUBE_VIDEO, + SOUNDCLOUD_TRACK, + NO_FORMATS_RESPONSE, + YOUTUBE_PLAYLIST, + ], +) +async def test_extract_media_service( + hass: HomeAssistant, + mock_youtube_dl: MockYoutubeDL, + snapshot: SnapshotAssertion, + empty_media_extractor_config: dict[str, Any], + url: str, +) -> None: + """Test play media service is registered.""" + await async_setup_component(hass, DOMAIN, empty_media_extractor_config) + await hass.async_block_till_done() + + assert ( + await hass.services.async_call( + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, + {ATTR_URL: url}, + blocking=True, + return_response=True, + ) + == snapshot + ) + + +async def test_extracting_playlist_no_entries( + hass: HomeAssistant, + mock_youtube_dl: MockYoutubeDL, + empty_media_extractor_config: dict[str, Any], +) -> None: + """Test extracting a playlist without entries.""" + + await async_setup_component(hass, DOMAIN, empty_media_extractor_config) + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, + {ATTR_URL: YOUTUBE_EMPTY_PLAYLIST}, + blocking=True, + return_response=True, + ) @pytest.mark.parametrize( diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 3d020b01c3d..d64161b8409 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index 3f347918f3d..4c507b4bd66 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( @@ -412,15 +412,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 0a21a8747e3..621838e8c67 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant, mock_login, mock_get_devices) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -64,7 +64,7 @@ async def test_form(hass: HomeAssistant, mock_login, mock_get_devices) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-email@test-domain.com" assert result2["data"] == { "username": "test-email@test-domain.com", @@ -90,7 +90,7 @@ async def test_form_errors( ) assert len(mock_login.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -114,7 +114,7 @@ async def test_form_response_errors( data={"username": "test-email@test-domain.com", "password": "test-password"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == message @@ -139,7 +139,7 @@ async def test_token_refresh(hass: HomeAssistant, mock_login, mock_get_devices) }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 @@ -174,7 +174,7 @@ async def test_token_reauthentication( }, data=mock_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_token_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -231,7 +231,7 @@ async def test_form_errors_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == reason mock_login.side_effect = None @@ -245,7 +245,7 @@ async def test_form_errors_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -290,7 +290,7 @@ async def test_client_errors_reauthentication( await hass.async_block_till_done() assert result["errors"]["base"] == reason - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM mock_login.side_effect = None with patch( @@ -303,5 +303,5 @@ async def test_client_errors_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index ae4e7b84288..377954c22df 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -29,7 +29,7 @@ async def test_user_step_no_devices( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" mock_setup_entry.assert_not_called() @@ -46,7 +46,7 @@ async def test_user_step_discovered_devices( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_device" with pytest.raises(vol.Invalid): @@ -58,7 +58,7 @@ async def test_user_step_discovered_devices( result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1} mock_setup_entry.assert_called_once() @@ -94,7 +94,7 @@ async def test_user_step_with_existing_device( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with pytest.raises(vol.Invalid): await hass.config_entries.flow.async_configure( @@ -115,7 +115,7 @@ async def test_bluetooth_discovered( data=FAKE_SERVICE_INFO_1, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == {"name": FAKE_ADDRESS_1} @@ -143,7 +143,7 @@ async def test_bluetooth_confirm( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FAKE_ADDRESS_1 assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1} diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index d0cd2cf8c5a..d5d61516c08 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -5,7 +5,7 @@ import json import pytest -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components import device_tracker from homeassistant.components.device_tracker import legacy from homeassistant.components.meraki.device_tracker import ( CONF_SECRET, diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 8d0b1620022..c494c4afeb9 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -31,7 +32,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -49,7 +50,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" default_data = result["data_schema"]({}) @@ -72,7 +73,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home" assert result["data"] == test_data @@ -100,7 +101,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["name"] == "already_configured" @@ -110,7 +111,7 @@ async def test_onboarding_step(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, data={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOME_LOCATION_NAME assert result["data"] == {"track_home": True} @@ -134,7 +135,7 @@ async def test_onboarding_step_abort_no_home( DOMAIN, context={"source": "onboarding"}, data={} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_home" @@ -153,7 +154,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: # Test show Options form result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # Test Options flow updated config entry @@ -164,7 +165,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry.entry_id, data=update_data ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Mock Title" assert result["data"] == update_data weatherdatamock.assert_called_with( diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index 8c5e7f43ced..cddc20b835a 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.met_eireann.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(name="met_eireann_setup", autouse=True) @@ -25,7 +26,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -43,7 +44,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER default_data = result["data_schema"]({}) @@ -66,7 +67,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == test_data.get("name") assert result["data"] == test_data @@ -87,11 +88,11 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result1["type"] is FlowResultType.CREATE_ENTRY # Create the second entry and assert that it is aborted result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 4b9e26f883b..5e6f3b845e9 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from meteofrance_api.model import Place import pytest -from homeassistant import data_entry_flow from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -114,7 +114,7 @@ async def test_user(hass: HomeAssistant, client_single) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with all provided with search returning only 1 place @@ -123,7 +123,7 @@ async def test_user(hass: HomeAssistant, client_single) -> None: context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_1_LAT}, {CITY_1_LON}" assert result["title"] == f"{CITY_1}" assert result["data"][CONF_LATITUDE] == str(CITY_1_LAT) @@ -139,14 +139,14 @@ async def test_user_list(hass: HomeAssistant, client_multiple) -> None: context={"source": SOURCE_USER}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cities" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_CITY: f"{CITY_3};{CITY_3_LAT};{CITY_3_LON}"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_3_LAT}, {CITY_3_LON}" assert result["title"] == f"{CITY_3}" assert result["data"][CONF_LATITUDE] == str(CITY_3_LAT) @@ -161,7 +161,7 @@ async def test_search_failed(hass: HomeAssistant, client_empty) -> None: data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_CITY: "empty"} @@ -179,5 +179,5 @@ async def test_abort_if_already_setup(hass: HomeAssistant, client_single) -> Non context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py index 42fb5e78d4a..ff9de358e86 100644 --- a/tests/components/meteoclimatic/test_config_flow.py +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -5,10 +5,10 @@ from unittest.mock import patch from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound import pytest -from homeassistant import data_entry_flow from homeassistant.components.meteoclimatic.const import CONF_STATION_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_STATION_CODE = "ESCAT4300000043206B" TEST_STATION_NAME = "Reus (Tarragona)" @@ -44,7 +44,7 @@ async def test_user(hass: HomeAssistant, client) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -53,7 +53,7 @@ async def test_user(hass: HomeAssistant, client) -> None: context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == TEST_STATION_CODE assert result["title"] == TEST_STATION_NAME assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE @@ -70,7 +70,7 @@ async def test_not_found(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -86,5 +86,5 @@ async def test_unknown_error(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index e4424b0b394..c2e75d89c1a 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -8,6 +8,7 @@ import requests_mock from homeassistant import config_entries from homeassistant.components.metoffice.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( METOFFICE_CONFIG_WAVERTREE, @@ -33,7 +34,7 @@ async def test_form(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -45,7 +46,7 @@ async def test_form(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_SITE_NAME_WAVERTREE assert result2["data"] == { "api_key": TEST_API_KEY, @@ -90,7 +91,7 @@ async def test_form_already_configured( data=METOFFICE_CONFIG_WAVERTREE, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -112,7 +113,7 @@ async def test_form_cannot_connect( {"api_key": TEST_API_KEY}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -132,5 +133,5 @@ async def test_form_unknown_error( {"api_key": TEST_API_KEY}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 24f6a52fa5c..49efdd5dc71 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the mFi sensor platform.""" from copy import deepcopy -import unittest.mock as mock +from unittest import mock from mficlient.client import FailedToLogin import pytest diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 6c69787beef..03b5d5f2c0a 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -1,6 +1,6 @@ """The tests for the mFi switch platform.""" -import unittest.mock as mock +from unittest import mock import pytest diff --git a/tests/components/microbees/test_config_flow.py b/tests/components/microbees/test_config_flow.py index 5e57c723f5b..327d0214f7a 100644 --- a/tests/components/microbees/test_config_flow.py +++ b/tests/components/microbees/test_config_flow.py @@ -37,7 +37,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -71,7 +71,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test@microbees.com" assert "result" in result assert result["result"].unique_id == 54321 @@ -101,7 +101,7 @@ async def test_config_non_unique_profile( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -129,7 +129,7 @@ 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["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_config_reauth_profile( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -190,7 +190,7 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -213,7 +213,7 @@ async def test_config_reauth_wrong_account( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -251,7 +251,7 @@ async def test_config_reauth_wrong_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -274,7 +274,7 @@ async def test_config_flow_with_invalid_credentials( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -299,7 +299,7 @@ async def test_config_flow_with_invalid_credentials( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "oauth_error" @@ -334,7 +334,7 @@ async def test_unexpected_exceptions( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -362,5 +362,5 @@ async def test_unexpected_exceptions( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index f48446e3e14..f34fde0c9a5 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import librouteros import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.mikrotik.const import ( CONF_ARP_PING, CONF_DETECTION_TIME, @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -75,14 +76,14 @@ async def test_flow_works(hass: HomeAssistant, api) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Mikrotik (0.0.0.0)" assert result["data"][CONF_HOST] == "0.0.0.0" assert result["data"][CONF_USERNAME] == "username" @@ -100,7 +101,7 @@ async def test_options(hass: HomeAssistant, api) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_tracker" result = await hass.config_entries.options.async_configure( @@ -112,7 +113,7 @@ async def test_options(hass: HomeAssistant, api) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_DETECTION_TIME: 30, CONF_ARP_PING: True, @@ -132,7 +133,7 @@ async def test_host_already_configured(hass: HomeAssistant, auth_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -145,7 +146,7 @@ async def test_connection_error(hass: HomeAssistant, conn_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -159,7 +160,7 @@ async def test_wrong_credentials(hass: HomeAssistant, auth_error) -> None: result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -183,7 +184,7 @@ async def test_reauth_success(hass: HomeAssistant, api) -> None: data=DEMO_USER_INPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {CONF_USERNAME: "username"} @@ -194,7 +195,7 @@ async def test_reauth_success(hass: HomeAssistant, api) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -215,7 +216,7 @@ async def test_reauth_failed(hass: HomeAssistant, auth_error) -> None: data=DEMO_USER_INPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -225,7 +226,7 @@ async def test_reauth_failed(hass: HomeAssistant, auth_error) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_PASSWORD: "invalid_auth", } @@ -248,7 +249,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant, conn_error) -> None data=DEMO_USER_INPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -258,5 +259,5 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant, conn_error) -> None }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 89dc37fd781..1eec2132a91 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -8,8 +8,7 @@ from typing import Any from freezegun import freeze_time import pytest -from homeassistant.components import mikrotik -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components import device_tracker, mikrotik from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 96ec0f5771e..cc6a737e75a 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -34,7 +34,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_hub_connection_error(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -49,7 +49,7 @@ async def test_hub_connection_error(hass: HomeAssistant, mock_api: MagicMock) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_hub_authentication_error( @@ -66,7 +66,7 @@ async def test_hub_authentication_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass: HomeAssistant) -> None: @@ -83,5 +83,5 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert entry.entry_id not in hass.data[DOMAIN] diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index d7740502412..832aaef3b19 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -17,7 +17,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -26,7 +26,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -35,7 +35,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -47,7 +47,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == { CONF_USERNAME: "user", @@ -74,7 +74,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -83,7 +83,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -92,7 +92,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -101,7 +101,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -110,7 +110,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("mill.Mill.connect", return_value=False): result = await hass.config_entries.flow.async_configure( @@ -121,7 +121,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -130,7 +130,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -139,7 +139,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -160,7 +160,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: ) test_data[CONNECTION_TYPE] = LOCAL - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == test_data[CONF_IP_ADDRESS] assert result["data"] == test_data @@ -182,7 +182,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -191,7 +191,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -211,7 +211,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -221,7 +221,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -230,7 +230,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -245,5 +245,5 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index 367c4cd717d..4a408524d09 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant, platform: str) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_config_flow(hass: HomeAssistant, platform: str) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My min_max" assert result["data"] == {} assert result["options"] == { @@ -93,7 +93,7 @@ async def test_options(hass: HomeAssistant, platform: str) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_ids") == input_sensors1 @@ -108,7 +108,7 @@ async def test_options(hass: HomeAssistant, platform: str) -> None: "type": "mean", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_ids": input_sensors2, "name": "My min_max", diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 4d86ee72cc6..c875697bf2f 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -51,7 +51,7 @@ async def test_default_name_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -80,7 +80,7 @@ async def test_min_sensor( entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -110,7 +110,7 @@ async def test_max_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -137,7 +137,7 @@ async def test_mean_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -164,7 +164,7 @@ async def test_mean_1_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_mean_4_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -215,7 +215,7 @@ async def test_median_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -242,7 +242,7 @@ async def test_range_4_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -268,7 +268,7 @@ async def test_range_1_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -394,7 +394,7 @@ async def test_last_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() state = hass.states.get("sensor.test_last") @@ -462,7 +462,7 @@ async def test_sensor_incorrect_state( entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -491,7 +491,7 @@ async def test_sum_sensor( entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -521,7 +521,7 @@ async def test_sum_sensor_no_state(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) 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 188b68ce5af..21136ac0815 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -51,7 +51,7 @@ async def test_address_validation_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -75,7 +75,7 @@ async def test_java_connection_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -95,7 +95,7 @@ async def test_bedrock_connection_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -119,7 +119,7 @@ async def test_java_connection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_ADDRESS] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_ADDRESS] == TEST_ADDRESS @@ -142,7 +142,7 @@ async def test_bedrock_connection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_ADDRESS] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_ADDRESS] == TEST_ADDRESS @@ -164,7 +164,7 @@ async def test_recovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with ( @@ -180,7 +180,7 @@ async def test_recovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=USER_INPUT ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_ADDRESS] assert result2["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS diff --git a/tests/components/minecraft_server/test_init.py b/tests/components/minecraft_server/test_init.py index 9c02fb56d91..6f7a49a190c 100644 --- a/tests/components/minecraft_server/test_init.py +++ b/tests/components/minecraft_server/test_init.py @@ -108,13 +108,11 @@ def create_v1_mock_binary_sensor_entity_entry( device_id=device_entry_id, ) assert entity_entry.unique_id == entity_unique_id - binary_sensor_entity_id_key_mapping = { + return { "entity_id": entity_entry.entity_id, "key": BINARY_SENSOR_KEYS["v2"], } - return binary_sensor_entity_id_key_mapping - async def test_setup_and_unload_entry( hass: HomeAssistant, java_mock_config_entry: MockConfigEntry @@ -134,12 +132,12 @@ async def test_setup_and_unload_entry( ): assert await hass.config_entries.async_setup(java_mock_config_entry.entry_id) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.LOADED + assert java_mock_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(java_mock_config_entry.entry_id) await hass.async_block_till_done() assert not hass.data.get(DOMAIN) - assert java_mock_config_entry.state == ConfigEntryState.NOT_LOADED + assert java_mock_config_entry.state is ConfigEntryState.NOT_LOADED async def test_setup_entry_lookup_failure( @@ -157,7 +155,7 @@ async def test_setup_entry_lookup_failure( ) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert java_mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_init_failure( @@ -175,7 +173,7 @@ async def test_setup_entry_init_failure( ) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert java_mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_not_ready( @@ -199,7 +197,7 @@ async def test_setup_entry_not_ready( ) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert java_mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_entry_migration( @@ -246,7 +244,7 @@ async def test_entry_migration( CONF_ADDRESS: TEST_ADDRESS, } assert migrated_config_entry.version == 3 - assert migrated_config_entry.state == ConfigEntryState.LOADED + assert migrated_config_entry.state is ConfigEntryState.LOADED # Test migrated device entry. device_entry = device_registry.async_get(device_entry_id) @@ -305,7 +303,7 @@ async def test_entry_migration_host_only( CONF_ADDRESS: TEST_HOST, } assert v1_mock_config_entry.version == 3 - assert v1_mock_config_entry.state == ConfigEntryState.LOADED + assert v1_mock_config_entry.state is ConfigEntryState.LOADED async def test_entry_migration_v3_failure( @@ -335,4 +333,4 @@ async def test_entry_migration_v3_failure( # Test config entry. assert v1_mock_config_entry.version == 2 - assert v1_mock_config_entry.state == ConfigEntryState.MIGRATION_ERROR + assert v1_mock_config_entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index a60df88b789..c2d5a9b014b 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -35,7 +35,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Spy cam" assert result2.get("data") == {} assert result2.get("options") == { @@ -80,7 +80,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_mjpeg_requests.get( @@ -96,7 +96,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"username": "invalid_auth"} @@ -114,7 +114,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Sky cam" assert result3.get("data") == {} assert result3.get("options") == { @@ -140,7 +140,7 @@ async def test_connection_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Test connectione error on MJPEG url @@ -156,7 +156,7 @@ async def test_connection_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"mjpeg_url": "cannot_connect"} @@ -179,7 +179,7 @@ async def test_connection_error( }, ) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "user" assert result3.get("errors") == {"still_image_url": "cannot_connect"} @@ -199,7 +199,7 @@ async def test_connection_error( }, ) - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "My cam" assert result4.get("data") == {} assert result4.get("options") == { @@ -236,7 +236,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -248,7 +248,7 @@ async def test_options_flow( """Test options config flow.""" result = await hass.config_entries.options.async_init(init_integration.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Register a second camera @@ -276,7 +276,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "init" assert result2.get("errors") == {"mjpeg_url": "already_configured"} @@ -294,7 +294,7 @@ async def test_options_flow( }, ) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "init" assert result3.get("errors") == {"mjpeg_url": "cannot_connect"} @@ -312,7 +312,7 @@ async def test_options_flow( }, ) - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4.get("step_id") == "init" assert result4.get("errors") == {"still_image_url": "cannot_connect"} @@ -331,7 +331,7 @@ async def test_options_flow( }, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5.get("step_id") == "init" assert result5.get("errors") == {"username": "invalid_auth"} @@ -347,7 +347,7 @@ async def test_options_flow( }, ) - assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("type") is FlowResultType.CREATE_ENTRY assert result6.get("data") == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_MJPEG_URL: "https://example.com/mjpeg", diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py index ab0825c884e..43840330313 100644 --- a/tests/components/moat/test_config_flow.py +++ b/tests/components/moat/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.moat.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Moat S2 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_not_moat(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_MOAT_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -48,7 +48,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -62,14 +62,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.moat.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Moat S2 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -85,7 +85,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -157,7 +157,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -170,7 +170,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -181,14 +181,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.moat.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Moat S2 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 4a5f472221f..f39c963b45b 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -2,7 +2,7 @@ from binascii import unhexlify from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -24,10 +24,6 @@ from homeassistant.setup import async_setup_component from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE from tests.common import async_capture_events, async_mock_service -from tests.components.conversation.conftest import mock_agent - -# To avoid autoflake8 removing the import -mock_agent = mock_agent @pytest.fixture @@ -321,7 +317,7 @@ async def test_webhook_handle_get_config( "time_zone": hass_config["time_zone"], "components": set(hass_config["components"]), "version": hass_config["version"], - "theme_color": "#03A9F4", # Default frontend theme color + "theme_color": ANY, "entities": { "mock-device-id": {"disabled": False}, "battery-state-id": {"disabled": False}, @@ -1027,14 +1023,18 @@ async def test_reregister_sensor( async def test_webhook_handle_conversation_process( - hass: HomeAssistant, homeassistant, create_registrations, webhook_client, mock_agent + hass: HomeAssistant, + homeassistant, + create_registrations, + webhook_client, + mock_conversation_agent, ) -> None: """Test that we can converse.""" webhook_client.server.app.router._frozen = False with patch( - "homeassistant.components.conversation.AgentManager.async_get_agent", - return_value=mock_agent, + "homeassistant.components.conversation.agent_manager.async_get_agent", + return_value=mock_conversation_agent, ): resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index b04f9a13933..872bd3a9d61 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -1,6 +1,6 @@ """The tests for the mochad light platform.""" -import unittest.mock as mock +from unittest import mock import pytest diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 96c3ba60b65..750dd48296e 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -1,6 +1,6 @@ """The tests for the mochad switch platform.""" -import unittest.mock as mock +from unittest import mock import pytest diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 62cf12958d3..1253a856bbf 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -47,12 +47,36 @@ class ReadResult: return False +@pytest.fixture(name="check_config_loaded") +def check_config_loaded_fixture(): + """Set default for check_config_loaded.""" + return True + + +@pytest.fixture(name="register_words") +def register_words_fixture(): + """Set default for register_words.""" + return [0x00, 0x00] + + +@pytest.fixture(name="config_addon") +def config_addon_fixture(): + """Add extra configuration items.""" + return None + + +@pytest.fixture(name="do_exception") +def do_exception_fixture(): + """Remove side_effect to pymodbus calls.""" + return False + + @pytest.fixture(name="mock_pymodbus") -def mock_pymodbus_fixture(): +def mock_pymodbus_fixture(do_exception, register_words): """Mock pymodbus.""" mock_pb = mock.AsyncMock() mock_pb.close = mock.MagicMock() - read_result = ReadResult([]) + read_result = ReadResult(register_words if register_words else []) mock_pb.read_coils.return_value = read_result mock_pb.read_discrete_inputs.return_value = read_result mock_pb.read_input_registers.return_value = read_result @@ -61,6 +85,16 @@ def mock_pymodbus_fixture(): mock_pb.write_registers.return_value = read_result mock_pb.write_coil.return_value = read_result mock_pb.write_coils.return_value = read_result + if do_exception: + exc = ModbusException("mocked pymodbus exception") + mock_pb.read_coils.side_effect = exc + mock_pb.read_discrete_inputs.side_effect = exc + mock_pb.read_input_registers.side_effect = exc + mock_pb.read_holding_registers.side_effect = exc + mock_pb.write_register.side_effect = exc + mock_pb.write_registers.side_effect = exc + mock_pb.write_coil.side_effect = exc + mock_pb.write_coils.side_effect = exc with ( mock.patch( "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", @@ -81,33 +115,9 @@ def mock_pymodbus_fixture(): yield mock_pb -@pytest.fixture(name="check_config_loaded") -def check_config_loaded_fixture(): - """Set default for check_config_loaded.""" - return True - - -@pytest.fixture(name="register_words") -def register_words_fixture(): - """Set default for register_words.""" - return [0x00, 0x00] - - -@pytest.fixture(name="config_addon") -def config_addon_fixture(): - """Add entra configuration items.""" - return None - - -@pytest.fixture(name="do_exception") -def do_exception_fixture(): - """Remove side_effect to pymodbus calls.""" - return False - - @pytest.fixture(name="mock_modbus") async def mock_modbus_fixture( - hass, caplog, register_words, check_config_loaded, config_addon, do_config + hass, caplog, check_config_loaded, config_addon, do_config, mock_pymodbus ): """Load integration modbus using mocked pymodbus.""" conf = copy.deepcopy(do_config) @@ -132,57 +142,23 @@ async def mock_modbus_fixture( } ] } - mock_pb = mock.AsyncMock() - mock_pb.close = mock.MagicMock() + now = dt_util.utcnow() with mock.patch( - "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", - return_value=mock_pb, + "homeassistant.helpers.event.dt_util.utcnow", + return_value=now, autospec=True, ): - now = dt_util.utcnow() - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", - return_value=now, - autospec=True, - ): - result = await async_setup_component(hass, DOMAIN, config) - assert result or not check_config_loaded - await hass.async_block_till_done() - yield mock_pb - - -@pytest.fixture(name="mock_pymodbus_exception") -async def mock_pymodbus_exception_fixture(hass, do_exception, mock_modbus): - """Trigger update call with time_changed event.""" - if do_exception: - exc = ModbusException("fail read_coils") - mock_modbus.read_coils.side_effect = exc - mock_modbus.read_discrete_inputs.side_effect = exc - mock_modbus.read_input_registers.side_effect = exc - mock_modbus.read_holding_registers.side_effect = exc - - -@pytest.fixture(name="mock_pymodbus_return") -async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): - """Trigger update call with time_changed event.""" - read_result = ReadResult(register_words if register_words else []) - mock_modbus.read_coils.return_value = read_result - mock_modbus.read_discrete_inputs.return_value = read_result - mock_modbus.read_input_registers.return_value = read_result - mock_modbus.read_holding_registers.return_value = read_result - mock_modbus.write_register.return_value = read_result - mock_modbus.write_registers.return_value = read_result - mock_modbus.write_coil.return_value = read_result - mock_modbus.write_coils.return_value = read_result - return mock_modbus + result = await async_setup_component(hass, DOMAIN, config) + assert result or not check_config_loaded + await hass.async_block_till_done() + return mock_pymodbus @pytest.fixture(name="mock_do_cycle") async def mock_do_cycle_fixture( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_pymodbus_exception, - mock_pymodbus_return, + mock_modbus, ) -> FrozenDateTimeFactory: """Trigger update call with time_changed event.""" freezer.tick(timedelta(seconds=1)) @@ -207,11 +183,12 @@ async def mock_test_state_fixture(hass, request): return request.param -@pytest.fixture(name="mock_ha") -async def mock_ha_fixture(hass, mock_pymodbus_return): +@pytest.fixture(name="mock_modbus_ha") +async def mock_modbus_ha_fixture(hass, mock_modbus): """Load homeassistant to allow service calls.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() + return mock_modbus @pytest.fixture(name="caplog_setup_text") diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 567618de3c6..7ae933998cf 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -207,7 +207,7 @@ async def test_all_binary_sensor(hass: HomeAssistant, expected, mock_do_cycle) - ], ) async def test_service_binary_sensor_update( - hass: HomeAssistant, mock_modbus, mock_ha + hass: HomeAssistant, mock_modbus_ha ) -> None: """Run test for service homeassistant.update_entity.""" @@ -217,7 +217,7 @@ async def test_service_binary_sensor_update( await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 3752358c071..94778cdcbd2 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -8,6 +8,8 @@ from homeassistant.components.climate.const import ( ATTR_FAN_MODES, ATTR_HVAC_MODE, ATTR_HVAC_MODES, + ATTR_SWING_MODE, + ATTR_SWING_MODES, FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, @@ -18,6 +20,11 @@ from homeassistant.components.climate.const import ( FAN_OFF, FAN_ON, FAN_TOP, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, HVACMode, ) from homeassistant.components.modbus.const import ( @@ -45,6 +52,13 @@ from homeassistant.components.modbus.const import ( CONF_HVAC_ONOFF_REGISTER, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -58,6 +72,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SLAVE, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -282,6 +297,41 @@ async def test_config_fan_mode_register(hass: HomeAssistant, mock_modbus) -> Non assert FAN_FOCUS not in state.attributes[ATTR_FAN_MODES] +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 11, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_OFF: 1, + CONF_SWING_MODE_SWING_BOTH: 2, + CONF_SWING_MODE_SWING_HORIZ: 3, + CONF_SWING_MODE_SWING_VERT: 4, + }, + }, + } + ], + }, + ], +) +async def test_config_swing_mode_register(hass: HomeAssistant, mock_modbus) -> None: + """Run configuration test for Fan mode register.""" + state = hass.states.get(ENTITY_ID) + assert SWING_ON in state.attributes[ATTR_SWING_MODES] + assert SWING_OFF in state.attributes[ATTR_SWING_MODES] + assert SWING_BOTH in state.attributes[ATTR_SWING_MODES] + assert SWING_HORIZONTAL in state.attributes[ATTR_SWING_MODES] + assert SWING_VERTICAL in state.attributes[ATTR_SWING_MODES] + + @pytest.mark.parametrize( "do_config", [ @@ -446,10 +496,10 @@ async def test_temperature_error(hass: HomeAssistant, expected, mock_do_cycle) - ], ) async def test_service_climate_update( - hass: HomeAssistant, mock_modbus, mock_ha, result, register_words + hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -561,10 +611,10 @@ async def test_service_climate_update( ], ) async def test_service_climate_fan_update( - hass: HomeAssistant, mock_modbus, mock_ha, result, register_words + hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -572,6 +622,146 @@ async def test_service_climate_fan_update( assert hass.states.get(ENTITY_ID).attributes[ATTR_FAN_MODE] == result +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_BOTH: 2, + }, + }, + }, + ] + }, + SWING_BOTH, + [0x02], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + }, + }, + }, + ] + }, + SWING_ON, + [0x01], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_HORIZ: 3, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 119, + }, + ] + }, + SWING_HORIZONTAL, + [0x03], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + CONF_SWING_MODE_SWING_BOTH: 3, + }, + }, + }, + ] + }, + SWING_OFF, + [0x00], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + CONF_SWING_MODE_SWING_BOTH: 3, + }, + }, + }, + ] + }, + STATE_UNKNOWN, + [0x05], + ), + ], +) +async def test_service_climate_swing_update( + hass: HomeAssistant, mock_modbus_ha, result, register_words +) -> None: + """Run test for service homeassistant.update_entity.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID).attributes[ATTR_SWING_MODE] == result + + @pytest.mark.parametrize( ("temperature", "result", "do_config"), [ @@ -654,10 +844,10 @@ async def test_service_climate_fan_update( ], ) async def test_service_climate_set_temperature( - hass: HomeAssistant, temperature, result, mock_modbus, mock_ha + hass: HomeAssistant, temperature, result, mock_modbus_ha ) -> None: """Test set_temperature.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_temperature", @@ -764,10 +954,10 @@ async def test_service_climate_set_temperature( ], ) async def test_service_set_hvac_mode( - hass: HomeAssistant, hvac_mode, result, mock_modbus, mock_ha + hass: HomeAssistant, hvac_mode, result, mock_modbus_ha ) -> None: """Test set HVAC mode.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, @@ -828,10 +1018,10 @@ async def test_service_set_hvac_mode( ], ) async def test_service_set_fan_mode( - hass: HomeAssistant, fan_mode, result, mock_modbus, mock_ha + hass: HomeAssistant, fan_mode, result, mock_modbus_ha ) -> None: """Test set Fan mode.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_fan_mode", @@ -843,6 +1033,69 @@ async def test_service_set_fan_mode( ) +@pytest.mark.parametrize( + ("swing_mode", "result", "do_config"), + [ + ( + SWING_OFF, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_OFF: 0, + }, + }, + } + ] + }, + ), + ( + SWING_ON, + [0x01], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_OFF: 0, + }, + }, + } + ] + }, + ), + ], +) +async def test_service_set_swing_mode( + hass: HomeAssistant, swing_mode, result, mock_modbus_ha +) -> None: + """Test set Swing mode.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) + await hass.services.async_call( + CLIMATE_DOMAIN, + "set_swing_mode", + { + "entity_id": ENTITY_ID, + ATTR_SWING_MODE: swing_mode, + }, + blocking=True, + ) + + test_value = State(ENTITY_ID, 35) test_value.attributes = {ATTR_TEMPERATURE: 37} diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index fa9e617d96d..0860b3136ba 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -182,13 +182,13 @@ async def test_register_cover(hass: HomeAssistant, expected, mock_do_cycle) -> N }, ], ) -async def test_service_cover_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_cover_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus_ha.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -255,30 +255,30 @@ async def test_restore_state_cover( }, ], ) -async def test_service_cover_move(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_cover_move(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus_ha.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OPEN - mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus_ha.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - await mock_modbus.reset() - mock_modbus.read_holding_registers.side_effect = ModbusException("fail write_") + await mock_modbus_ha.reset() + mock_modbus_ha.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) - assert mock_modbus.read_holding_registers.called + assert mock_modbus_ha.read_holding_registers.called assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - mock_modbus.read_coils.side_effect = ModbusException("fail write_") + mock_modbus_ha.read_coils.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 9719de3601b..d52b9dc309a 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -262,7 +262,6 @@ async def test_fan_service_turn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - mock_pymodbus_return, ) -> None: """Run test for service turn_on/turn_off.""" @@ -323,13 +322,13 @@ async def test_fan_service_turn( }, ], ) -async def test_service_fan_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_fan_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 2c5810a7757..82c65576f02 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -67,6 +67,11 @@ from homeassistant.components.modbus.const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_VIRTUAL_COUNT, DEFAULT_SCAN_INTERVAL, @@ -85,6 +90,7 @@ from homeassistant.components.modbus.validators import ( check_config, check_hvac_target_temp_registers, duplicate_fan_mode_validator, + duplicate_swing_mode_validator, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -630,6 +636,42 @@ async def test_check_config_sensor(hass: HomeAssistant, do_config) -> None: ], } ], + [ # Testing Swing modes + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 120, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_BOTH: 1, + }, + }, + }, + { + CONF_NAME: TEST_ENTITY_NAME + " 2", + CONF_ADDRESS: 119, + CONF_SLAVE: 0, + CONF_TARGET_TEMP: 118, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [120], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_BOTH: 1, + }, + }, + }, + ], + } + ], [ { CONF_NAME: TEST_MODBUS_NAME, @@ -734,6 +776,29 @@ async def test_check_config_climate(hass: HomeAssistant, do_config) -> None: CONF_FAN_MODE_REGISTER: { CONF_ADDRESS: 117, }, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 117, + }, + }, + ], + [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1, + CONF_TARGET_TEMP: [117], + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 117, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_HEAT_COOL: 2, + CONF_HVAC_MODE_DRY: 3, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 117, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [117], + }, }, ], ], @@ -744,6 +809,7 @@ async def test_climate_conflict_addresses(do_config) -> None: assert CONF_HVAC_MODE_REGISTER not in do_config[0] assert CONF_HVAC_ONOFF_REGISTER not in do_config[0] assert CONF_FAN_MODE_REGISTER not in do_config[0] + assert CONF_SWING_MODE_REGISTER not in do_config[0] @pytest.mark.parametrize( @@ -765,6 +831,25 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 11, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 7, + CONF_SWING_MODE_SWING_OFF: 9, + CONF_SWING_MODE_SWING_BOTH: 9, + }, + } + ], +) +async def test_duplicate_swing_mode_validator(do_config) -> None: + """Test duplicate modbus validator.""" + duplicate_swing_mode_validator(do_config) + assert len(do_config[CONF_SWING_MODE_VALUES]) == 2 + + @pytest.mark.parametrize( ("do_config", "sensor_cnt"), [ @@ -1281,7 +1366,6 @@ async def mock_modbus_read_pymodbus_fixture( do_type, do_scan_interval, do_return, - do_exception, caplog, mock_pymodbus, freezer: FrozenDateTimeFactory, @@ -1289,10 +1373,6 @@ async def mock_modbus_read_pymodbus_fixture( """Load integration modbus using mocked pymodbus.""" caplog.clear() caplog.set_level(logging.ERROR) - mock_pymodbus.read_coils.side_effect = do_exception - mock_pymodbus.read_discrete_inputs.side_effect = do_exception - mock_pymodbus.read_input_registers.side_effect = do_exception - mock_pymodbus.read_holding_registers.side_effect = do_exception mock_pymodbus.read_coils.return_value = do_return mock_pymodbus.read_discrete_inputs.return_value = do_return mock_pymodbus.read_input_registers.return_value = do_return @@ -1561,7 +1641,7 @@ async def test_shutdown( ], ) async def test_stop_restart( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus ) -> None: """Run test for service stop.""" @@ -1572,7 +1652,7 @@ async def test_stop_restart( await hass.async_block_till_done() assert hass.states.get(entity_id).state == "17" - mock_pymodbus_return.reset_mock() + mock_modbus.reset_mock() caplog.clear() data = { ATTR_HUB: TEST_MODBUS_NAME, @@ -1580,23 +1660,23 @@ async def test_stop_restart( await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - assert mock_pymodbus_return.close.called + assert mock_modbus.close.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text - mock_pymodbus_return.reset_mock() + mock_modbus.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert not mock_pymodbus_return.close.called - assert mock_pymodbus_return.connect.called + assert not mock_modbus.close.called + assert mock_modbus.connect.called assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text - mock_pymodbus_return.reset_mock() + mock_modbus.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert mock_pymodbus_return.close.called - assert mock_pymodbus_return.connect.called + assert mock_modbus.close.called + assert mock_modbus.connect.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text @@ -1626,7 +1706,7 @@ async def test_write_no_client(hass: HomeAssistant, mock_modbus) -> None: async def test_integration_reload( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_pymodbus_return, + mock_modbus, freezer: FrozenDateTimeFactory, ) -> None: """Run test for integration reload.""" @@ -1647,7 +1727,7 @@ async def test_integration_reload( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_reload_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus ) -> None: """Run test for integration connect failure on reload.""" caplog.set_level(logging.INFO) @@ -1656,9 +1736,7 @@ async def test_integration_reload_failed( yaml_path = get_fixture_path("configuration.yaml", "modbus") with ( mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), - mock.patch.object( - mock_pymodbus_return, "connect", side_effect=ModbusException("error") - ), + 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() @@ -1669,7 +1747,7 @@ async def test_integration_reload_failed( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_setup_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus ) -> None: """Run test for integration setup on reload.""" with mock.patch.object( diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index e5e1b56d77b..e74da085180 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -262,7 +262,6 @@ async def test_light_service_turn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - mock_pymodbus_return, ) -> None: """Run test for service turn_on/turn_off.""" @@ -300,12 +299,6 @@ async def test_light_service_turn( ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE - mock_modbus.write_coil.side_effect = ModbusException("fail write_") - await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": ENTITY_ID} - ) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE @pytest.mark.parametrize( @@ -323,13 +316,13 @@ async def test_light_service_turn( }, ], ) -async def test_service_light_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_light_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 524acc0dabb..71cb64cc1b6 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1391,14 +1391,14 @@ async def test_restore_state_sensor( }, ], ) -async def test_service_sensor_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_sensor_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_input_registers.return_value = ReadResult([27]) + mock_modbus_ha.read_input_registers.return_value = ReadResult([27]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == "27" - mock_modbus.read_input_registers.return_value = ReadResult([32]) + mock_modbus_ha.read_input_registers.return_value = ReadResult([32]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 4eb0a5b3a18..bdb95c667c7 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -277,7 +277,6 @@ async def test_switch_service_turn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - mock_pymodbus_return, ) -> None: """Run test for service turn_on/turn_off.""" assert MODBUS_DOMAIN in hass.config.components @@ -337,13 +336,13 @@ async def test_switch_service_turn( }, ], ) -async def test_service_switch_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_switch_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -368,9 +367,7 @@ async def test_service_switch_update(hass: HomeAssistant, mock_modbus, mock_ha) }, ], ) -async def test_delay_switch( - hass: HomeAssistant, mock_modbus, mock_pymodbus_return -) -> None: +async def test_delay_switch(hass: HomeAssistant, mock_modbus) -> None: """Run test for switch verify delay.""" mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) now = dt_util.utcnow() diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index aeb8fb6d966..2ae4d6659e7 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -4,12 +4,12 @@ from unittest.mock import MagicMock, patch import phone_modem -from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.modem_callerid.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import com_port, patch_config_flow_modem @@ -38,14 +38,14 @@ async def test_flow_usb(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: com_port().device} @@ -57,7 +57,7 @@ async def test_flow_usb_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -79,7 +79,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( @@ -87,7 +87,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -108,7 +108,7 @@ async def test_flow_user_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_flow_user_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} @@ -130,7 +130,7 @@ async def test_flow_user_no_port_list(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -142,7 +142,7 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -151,5 +151,5 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant) -> None: data={}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/modem_callerid/test_init.py b/tests/components/modem_callerid/test_init.py index ccf97f60e10..e12850f763d 100644 --- a/tests/components/modem_callerid/test_init.py +++ b/tests/components/modem_callerid/test_init.py @@ -30,7 +30,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: patch("phone_modem.PhoneModem._modem_sm"), ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: @@ -45,7 +45,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: modemmock.side_effect = exceptions.SerialError await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) diff --git a/tests/components/modern_forms/__init__.py b/tests/components/modern_forms/__init__.py index 65de87c333d..ae4e5bd9862 100644 --- a/tests/components/modern_forms/__init__.py +++ b/tests/components/modern_forms/__init__.py @@ -19,10 +19,9 @@ async def modern_forms_call_mock(method, url, data): fixture = "modern_forms/device_info.json" else: fixture = "modern_forms/device_status.json" - response = AiohttpClientMockResponse( + return AiohttpClientMockResponse( method=method, url=url, json=json.loads(load_fixture(fixture)) ) - return response async def modern_forms_no_light_call_mock(method, url, data): @@ -31,10 +30,9 @@ async def modern_forms_no_light_call_mock(method, url, data): fixture = "modern_forms/device_info_no_light.json" else: fixture = "modern_forms/device_status_no_light.json" - response = AiohttpClientMockResponse( + return AiohttpClientMockResponse( method=method, url=url, json=json.loads(load_fixture(fixture)) ) - return response async def modern_forms_timers_set_mock(method, url, data): @@ -43,10 +41,9 @@ async def modern_forms_timers_set_mock(method, url, data): fixture = "modern_forms/device_info.json" else: fixture = "modern_forms/device_status_timers_active.json" - response = AiohttpClientMockResponse( + return AiohttpClientMockResponse( method=method, url=url, json=json.loads(load_fixture(fixture)) ) - return response async def init_integration( diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 6e1d2452479..56c293b241a 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -35,7 +35,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM with patch( "homeassistant.components.modern_forms.async_setup_entry", @@ -47,7 +47,7 @@ async def test_full_user_flow_implementation( assert result2.get("title") == "ModernFormsFan" assert "data" in result2 - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_HOST] == "192.168.1.123" assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" assert len(mock_setup_entry.mock_calls) == 1 @@ -82,7 +82,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "example"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM flow = flows[0] assert "context" in flow @@ -94,7 +94,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "example" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -117,7 +117,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -146,7 +146,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -178,7 +178,7 @@ async def test_zeroconf_confirm_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -214,7 +214,7 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -248,5 +248,5 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index fcce2d139d2..33c67421958 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -29,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_BASE_NAME assert result2["data"] == {"host": MOCK_BASE_HOST} assert len(mock_setup_entry.mock_calls) == 1 @@ -68,7 +68,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: data={"host": MOCK_BASE_HOST}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -98,5 +98,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index 0acea3d03e6..760d82dfedc 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -2,11 +2,11 @@ import pytest +from homeassistant.components import sensor from homeassistant.components.mold_indicator.sensor import ( ATTR_CRITICAL_TEMP, ATTR_DEWPOINT, ) -import homeassistant.components.sensor as sensor from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 74c69078b1d..fafdb3c2ecf 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.monoprice.const import ( CONF_SOURCE_1, CONF_SOURCE_4, @@ -14,6 +14,7 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -31,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -49,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_PORT] assert result2["data"] == { CONF_PORT: CONFIG[CONF_PORT], @@ -72,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -90,7 +91,7 @@ async def test_generic_exception(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -112,7 +113,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -120,5 +121,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 8fbab51f5a2..aac4381de59 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -27,7 +27,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Moon" assert result2.get("data") == {} @@ -43,5 +43,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py index 8de1fd81add..826fe8db2aa 100644 --- a/tests/components/mopeka/test_config_flow.py +++ b/tests/components/mopeka/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_not_mopeka(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_MOPEKA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -48,7 +48,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -62,14 +62,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -85,7 +85,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -157,7 +157,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -170,7 +170,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -181,14 +181,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 8d290b0b380..4168c3a1f63 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.motion_blinds import const from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -132,7 +133,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -141,7 +142,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -150,7 +151,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -165,7 +166,7 @@ async def test_config_flow_discovery_1_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -174,7 +175,7 @@ async def test_config_flow_discovery_1_success(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -187,7 +188,7 @@ async def test_config_flow_discovery_1_success(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -202,7 +203,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -215,7 +216,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["data_schema"].schema["select_ip"].container == [ TEST_HOST, @@ -228,7 +229,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: {"select_ip": TEST_HOST2}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -241,7 +242,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST2, @@ -256,7 +257,7 @@ async def test_config_flow_connection_error(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -265,7 +266,7 @@ async def test_config_flow_connection_error(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -278,7 +279,7 @@ async def test_config_flow_connection_error(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -288,7 +289,7 @@ async def test_config_flow_discovery_fail(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -301,7 +302,7 @@ async def test_config_flow_discovery_fail(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} @@ -312,7 +313,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -321,7 +322,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -334,7 +335,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -355,7 +356,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -368,7 +369,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -393,7 +394,7 @@ async def test_dhcp_flow_abort(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_motionblinds" @@ -413,7 +414,7 @@ async def test_dhcp_flow_abort_invalid_response(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_motionblinds" @@ -435,7 +436,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -443,7 +444,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={const.CONF_WAIT_FOR_PUSH: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } @@ -467,7 +468,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -476,7 +477,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST2}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -485,7 +486,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY2}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_API_KEY] == TEST_API_KEY2 assert config_entry.data[const.CONF_INTERFACE] == TEST_HOST_ANY diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index f540fdf421c..887d20d71ce 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -4,11 +4,12 @@ from unittest.mock import patch from motionblindsble.const import MotionBlindType -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries 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 homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME @@ -48,7 +49,7 @@ async def test_config_flow_manual_success( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -56,14 +57,14 @@ async def test_config_flow_manual_success( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is 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["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, @@ -83,7 +84,7 @@ async def test_config_flow_manual_error_invalid_mac( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -92,7 +93,7 @@ async def test_config_flow_manual_error_invalid_mac( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": const.ERROR_INVALID_MAC_CODE} @@ -101,7 +102,7 @@ async def test_config_flow_manual_error_invalid_mac( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # Finish flow @@ -109,7 +110,7 @@ async def test_config_flow_manual_error_invalid_mac( result["flow_id"], {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, @@ -133,14 +134,14 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( 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["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -152,7 +153,7 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER @@ -165,7 +166,7 @@ async def test_config_flow_manual_error_could_not_find_motor( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -175,7 +176,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": const.ERROR_COULD_NOT_FIND_MOTOR} @@ -185,7 +186,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # Finish flow @@ -193,7 +194,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result["flow_id"], {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, @@ -213,7 +214,7 @@ async def test_config_flow_manual_error_no_devices_found( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -223,7 +224,7 @@ async def test_config_flow_manual_error_no_devices_found( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_DEVICES_FOUND @@ -237,7 +238,7 @@ async def test_config_flow_bluetooth_success( data=BLIND_SERVICE_INFO, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -245,7 +246,7 @@ async def test_config_flow_bluetooth_success( {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 7163f2c8152..816fb31933a 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -8,7 +8,7 @@ from motioneye_client.client import ( MotionEyeClientRequestError, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, @@ -22,6 +22,7 @@ from homeassistant.components.motioneye.const import ( ) from homeassistant.const import CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_URL, create_mock_motioneye_client, create_mock_motioneye_config_entry @@ -34,7 +35,7 @@ async def test_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -61,7 +62,7 @@ async def test_user_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_URL}" assert result["data"] == { CONF_URL: TEST_URL, @@ -88,12 +89,12 @@ async def test_hassio_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "motionEye"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == data_entry_flow.FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" mock_client = create_mock_motioneye_client() @@ -119,7 +120,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Add-on" assert result3.get("data") == { CONF_URL: TEST_URL, @@ -159,7 +160,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert mock_client.async_client_close.called @@ -188,7 +189,7 @@ async def test_user_invalid_url(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_url"} @@ -219,7 +220,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert mock_client.async_client_close.called @@ -249,7 +250,7 @@ async def test_user_request_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert mock_client.async_client_close.called @@ -271,7 +272,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -300,7 +301,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert dict(config_entry.data) == {**new_data, CONF_WEBHOOK_ID: "test-webhook-id"} @@ -328,7 +329,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -350,7 +351,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called @@ -372,7 +373,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -392,7 +393,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -401,7 +402,7 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -413,7 +414,7 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result2.get("type") == data_entry_flow.FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_in_progress" @@ -430,12 +431,12 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2.get("type") == data_entry_flow.FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM mock_client = create_mock_motioneye_client() @@ -461,7 +462,7 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() @@ -487,7 +488,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -498,7 +499,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -533,7 +534,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -552,7 +553,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert result["data"][CONF_STREAM_URL_TEMPLATE] == "http://moo" diff --git a/tests/components/motionmount/test_config_flow.py b/tests/components/motionmount/test_config_flow.py index f24c4e7a2e4..4de23de63c9 100644 --- a/tests/components/motionmount/test_config_flow.py +++ b/tests/components/motionmount/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_user_connection_error( @@ -56,7 +56,7 @@ async def test_user_connection_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -75,7 +75,7 @@ async def test_user_connection_error_invalid_hostname( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -94,7 +94,7 @@ async def test_user_timeout_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "time_out" @@ -113,7 +113,7 @@ async def test_user_not_connected_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_connected" @@ -134,7 +134,7 @@ async def test_user_response_error_single_device_old_ce_old_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -162,7 +162,7 @@ async def test_user_response_error_single_device_new_ce_old_pro( data=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] @@ -188,7 +188,7 @@ async def test_user_response_error_single_device_new_ce_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] @@ -219,7 +219,7 @@ async def test_user_response_error_multi_device_old_ce_old_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -242,7 +242,7 @@ async def test_user_response_error_multi_device_new_ce_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -261,7 +261,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -280,7 +280,7 @@ async def test_zeroconf_connection_error_invalid_hostname( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -299,7 +299,7 @@ async def test_zeroconf_timout_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "time_out" @@ -318,7 +318,7 @@ async def test_zeroconf_not_connected_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_connected" @@ -339,7 +339,7 @@ async def test_show_zeroconf_form_old_ce_old_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -360,7 +360,7 @@ async def test_show_zeroconf_form_old_ce_new_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -381,7 +381,7 @@ async def test_show_zeroconf_form_new_ce_old_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -400,7 +400,7 @@ async def test_show_zeroconf_form_new_ce_new_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -419,7 +419,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -437,14 +437,14 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT.copy(), ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] @@ -471,13 +471,13 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 1224fce098d..821a3f911b7 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -148,74 +148,6 @@ async def test_preset_none_in_preset_modes( assert "preset_modes must not include preset mode 'none'" in caplog.text -@pytest.mark.parametrize( - ("hass_config", "parameter"), - [ - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"away_mode_command_topic": "away-mode-command-topic"},), - ), - "away_mode_command_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"away_mode_state_topic": "away-mode-state-topic"},), - ), - "away_mode_state_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"away_mode_state_template": "{{ value_json }}"},), - ), - "away_mode_state_template", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_command_topic": "hold-mode-command-topic"},), - ), - "hold_mode_command_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_command_template": "hold-mode-command-template"},), - ), - "hold_mode_command_template", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_state_topic": "hold-mode-state-topic"},), - ), - "hold_mode_state_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_state_template": "{{ value_json }}"},), - ), - "hold_mode_state_template", - ), - ], -) -async def test_preset_modes_deprecation_guard( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, parameter: str -) -> None: - """Test the configuration for invalid legacy parameters.""" - assert f"[{parameter}] is an invalid option for [mqtt]. Check: mqtt->mqtt->climate->0->{parameter}" - - @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_supported_features( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 9dc52871529..e9c3b57777f 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -83,7 +83,7 @@ def help_all_subscribe_calls(mqtt_client_mock: MqttMockPahoClient) -> list[Any]: def help_custom_config( mqtt_entity_domain: str, mqtt_base_config: ConfigType, - mqtt_entity_configs: Iterable[ConfigType,], + mqtt_entity_configs: Iterable[ConfigType], ) -> ConfigType: """Tweak a default config for parametrization. diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 719117e59a9..422ec84c091 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -11,10 +11,12 @@ from uuid import uuid4 import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo +from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient @@ -185,13 +187,13 @@ async def test_user_connection_works( result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, @@ -216,7 +218,7 @@ async def test_user_v5_connection_works( "mqtt", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1", "advanced_options": True} @@ -232,7 +234,7 @@ async def test_user_v5_connection_works( mqtt.CONF_PROTOCOL: "5", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "another-broker", "discovery": True, @@ -254,13 +256,13 @@ async def test_user_connection_fails( result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection @@ -284,13 +286,13 @@ async def test_manual_config_set( result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1", "port": "1883"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, @@ -317,7 +319,7 @@ async def test_user_single_instance(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -328,7 +330,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_HASSIO} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -354,7 +356,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -384,7 +386,7 @@ async def test_hassio_confirm( ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -393,7 +395,7 @@ async def test_hassio_confirm( result["flow_id"], {"discovery": True} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "mock-broker", "port": 1883, @@ -433,7 +435,7 @@ async def test_hassio_cannot_connect( ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -442,7 +444,7 @@ async def test_hassio_cannot_connect( result["flow_id"], {"discovery": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection assert len(mock_try_connection_time_out.mock_calls) @@ -473,7 +475,7 @@ async def test_option_flow( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -485,7 +487,7 @@ async def test_option_flow( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -510,7 +512,7 @@ async def test_option_flow( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -609,7 +611,7 @@ async def test_bad_certificate( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -682,7 +684,7 @@ async def test_keepalive_validation( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" if error: @@ -722,7 +724,7 @@ async def test_disable_birth_will( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -734,7 +736,7 @@ async def test_disable_birth_will( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -757,7 +759,7 @@ async def test_disable_birth_will( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -799,7 +801,7 @@ async def test_invalid_discovery_prefix( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -809,7 +811,7 @@ async def test_invalid_discovery_prefix( mqtt.CONF_PORT: 2345, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -822,7 +824,7 @@ async def test_invalid_discovery_prefix( mqtt.CONF_DISCOVERY_PREFIX: "homeassistant#invalid", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" assert result["errors"]["base"] == "bad_discovery_prefix" assert config_entry.data == { @@ -892,7 +894,7 @@ async def test_option_flow_default_suggested_values( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -900,7 +902,7 @@ async def test_option_flow_default_suggested_values( } suggested = { mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, } for key, value in defaults.items(): assert get_default(result["data_schema"].schema, key) == value @@ -916,7 +918,7 @@ async def test_option_flow_default_suggested_values( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: True, @@ -950,11 +952,11 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Test updated default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "another-broker", @@ -962,7 +964,7 @@ async def test_option_flow_default_suggested_values( } suggested = { mqtt.CONF_USERNAME: "us3r", - mqtt.CONF_PASSWORD: "p4ss", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, } for key, value in defaults.items(): assert get_default(result["data_schema"].schema, key) == value @@ -973,7 +975,7 @@ async def test_option_flow_default_suggested_values( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: False, @@ -1007,7 +1009,7 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Make sure all MQTT related jobs are done before ending the test await hass.async_block_till_done() @@ -1049,7 +1051,7 @@ async def test_skipping_advanced_options( 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["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -1059,6 +1061,102 @@ async def test_skipping_advanced_options( assert result["step_id"] == step_id +@pytest.mark.parametrize( + ("test_input", "user_input", "new_password"), + [ + ( + { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: "verysecret", + }, + { + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: "newpassword", + }, + "newpassword", + ), + ( + { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: "verysecret", + }, + { + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, + }, + "verysecret", + ), + ], +) +async def test_step_reauth( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, + mock_try_connection: MagicMock, + mock_reload_after_entry_update: MagicMock, + test_input: dict[str, Any], + user_input: dict[str, Any], + new_password: str, +) -> None: + """Test that the reauth step works.""" + + # Prepare the config entry + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + hass.config_entries.async_update_entry( + config_entry, + data=test_input, + ) + await mqtt_mock_entry() + + # Start reauth flow + 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" + assert result["context"]["source"] == "reauth" + + # Show the form + result = await hass.config_entries.flow.async_init( + mqtt.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # Simulate re-auth fails + mock_try_connection.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + # Simulate re-auth succeeds + mock_try_connection.return_value = True + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert config_entry.data.get(mqtt.CONF_PASSWORD) == new_password + await hass.async_block_till_done() + + async def test_options_user_connection_fails( hass: HomeAssistant, mock_try_connection_time_out: MagicMock ) -> None: @@ -1073,7 +1171,7 @@ async def test_options_user_connection_fails( }, ) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM mock_try_connection_time_out.reset_mock() result = await hass.config_entries.options.async_configure( @@ -1081,7 +1179,7 @@ async def test_options_user_connection_fails( user_input={mqtt.CONF_BROKER: "bad-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection @@ -1110,21 +1208,21 @@ async def test_options_bad_birth_message_fails( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"birth_topic": "ha_state/online/#"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "bad_birth" # Check config entry did not update @@ -1151,21 +1249,21 @@ async def test_options_bad_will_message_fails( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"will_topic": "ha_state/offline/#"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "bad_will" # Check config entry did not update @@ -1221,7 +1319,7 @@ async def test_try_connection_with_advanced_parameters( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -1231,7 +1329,7 @@ async def test_try_connection_with_advanced_parameters( } suggested = { mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_PROTOCOL: "3.1.1", mqtt.CONF_TRANSPORT: "websockets", @@ -1261,7 +1359,7 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_WS_HEADERS: '{"h3": "v3"}', }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "options" await hass.async_block_till_done() @@ -1292,7 +1390,7 @@ async def test_try_connection_with_advanced_parameters( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -1320,7 +1418,7 @@ async def test_setup_with_advanced_settings( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["data_schema"].schema["advanced_options"] @@ -1335,7 +1433,7 @@ async def test_setup_with_advanced_settings( "advanced_options": True, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert "advanced_options" not in result["data_schema"].schema assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID] @@ -1364,7 +1462,7 @@ async def test_setup_with_advanced_settings( mqtt.CONF_TRANSPORT: "websockets", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert "advanced_options" not in result["data_schema"].schema assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID] @@ -1399,7 +1497,7 @@ async def test_setup_with_advanced_settings( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["errors"]["base"] == "bad_ws_headers" @@ -1424,7 +1522,7 @@ async def test_setup_with_advanced_settings( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( @@ -1434,7 +1532,7 @@ async def test_setup_with_advanced_settings( mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY # Check config entry result assert config_entry.data == { @@ -1481,7 +1579,7 @@ async def test_change_websockets_transport_to_tcp( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["data_schema"].schema["transport"] assert result["data_schema"].schema["ws_path"] @@ -1498,7 +1596,7 @@ async def test_change_websockets_transport_to_tcp( mqtt.CONF_WS_PATH: "/some_path", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( @@ -1508,7 +1606,7 @@ async def test_change_websockets_transport_to_tcp( mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY # Check config entry result assert config_entry.data == { diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 4f4c9a18bd9..465e87205fa 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -5,7 +5,7 @@ import json import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info from homeassistant.core import HomeAssistant, ServiceCall diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 24891895fad..a00af080bf1 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1487,6 +1487,7 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( await async_start(hass, "homeassistant", entry) await hass.async_block_till_done() await hass.async_block_till_done() + await hass.async_block_till_done() assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) assert not mqtt_client_mock.unsubscribe.called @@ -1537,6 +1538,7 @@ async def test_mqtt_discovery_unsubscribe_once( await async_start(hass, "homeassistant", entry) await hass.async_block_till_done() await hass.async_block_till_done() + await hass.async_block_till_done() assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) assert not mqtt_client_mock.unsubscribe.called diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 75baca046bd..c29250bff82 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -101,7 +101,7 @@ async def async_turn_off(hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL) -> Non async def async_set_mode( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, mode: str = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, mode: str | None = None ) -> None: """Set mode for all or specified humidifier.""" data = { @@ -114,7 +114,7 @@ async def async_set_mode( async def async_set_humidity( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, humidity: int = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, humidity: int | None = None ) -> None: """Set target humidity for all or specified humidifier.""" data = { diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a9f2ba4354b..a1264b52739 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3,19 +3,23 @@ import asyncio from copy import deepcopy from datetime import datetime, timedelta -from functools import partial import json +import socket import ssl from typing import Any, TypedDict from unittest.mock import ANY, MagicMock, call, mock_open, patch from freezegun.api import FrozenDateTimeFactory +import paho.mqtt.client as paho_mqtt import pytest import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import EnsureJobAfterCooldown +from homeassistant.components.mqtt.client import ( + RECONNECT_INTERVAL_SECONDS, + EnsureJobAfterCooldown, +) from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ( MessageCallbackType, @@ -84,12 +88,6 @@ class _DebugInfo(TypedDict): config: _DebugDeviceInfo -class RecordCallsPartial(partial[Any]): - """Wrapper class for partial.""" - - __name__ = "RecordCallPartialTest" - - @pytest.fixture(autouse=True) def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" @@ -143,17 +141,17 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( assert mqtt_client_mock.connect.call_count == 1 -async def test_mqtt_disconnects_on_home_assistant_stop( +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, ) -> None: - """Test if client stops on HA stop.""" + """Test if client is not disconnected on HA stop.""" await mqtt_mock_entry() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_client_mock.loop_stop.call_count == 1 + assert mqtt_client_mock.disconnect.call_count == 0 async def test_mqtt_await_ack_at_disconnect( @@ -168,8 +166,14 @@ async def test_mqtt_await_ack_at_disconnect( rc = 0 with patch("paho.mqtt.client.Client") as mock_client: - mock_client().connect = MagicMock(return_value=0) - mock_client().publish = MagicMock(return_value=FakeInfo()) + mqtt_client = mock_client.return_value + mqtt_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 + ), + ) + mqtt_client.publish = MagicMock(return_value=FakeInfo()) entry = MockConfigEntry( domain=mqtt.DOMAIN, data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, @@ -1676,6 +1680,7 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( the subscribe cool down period has ended. """ mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock.subscribe.reset_mock() # Fake that the client is connected mqtt_mock().connected = True @@ -1868,6 +1873,7 @@ async def test_reload_entry_with_restored_subscriptions( # Setup the MQTT entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) mqtt_client_mock.connect.return_value = 0 with patch("homeassistant.config.load_yaml_config_file", return_value={}): await entry.async_setup(hass) @@ -1932,6 +1938,7 @@ async def test_canceling_debouncer_on_shutdown( """Test canceling the debouncer when HA shuts down.""" mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock.subscribe.reset_mock() # Fake that the client is connected mqtt_mock().connected = True @@ -2015,7 +2022,7 @@ async def test_initial_setup_logs_error( """Test for setup failure if initial client connection fails.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - mqtt_client_mock.connect.return_value = 1 + mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) try: assert await hass.config_entries.async_setup(entry.entry_id) except HomeAssistantError: @@ -2040,6 +2047,24 @@ async def test_logs_error_if_no_connect_broker( ) +@pytest.mark.parametrize("return_code", [4, 5]) +async def test_triggers_reauth_flow_if_auth_fails( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, + return_code: int, +) -> None: + """Test re-auth is triggered if authentication is failing.""" + await mqtt_mock_entry() + # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD + mqtt_client_mock.on_connect(mqtt_client_mock, None, None, return_code) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + + @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) async def test_handle_mqtt_on_callback( hass: HomeAssistant, @@ -2237,7 +2262,12 @@ async def test_handle_mqtt_timeout_on_callback( mock_client = mock_client.return_value mock_client.publish.return_value = FakeInfo() mock_client.subscribe.side_effect = _mock_ack - mock_client.connect.return_value = 0 + mock_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ), + ) entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} @@ -2505,6 +2535,75 @@ async def test_delayed_birth_message( ) +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "homeassistant/status", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +async def test_subscription_done_when_birth_message_is_sent( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_config_entry_data, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_mock = await mqtt_mock_entry() + + hass.set_state(CoreState.starting) + birth = asyncio.Event() + + await hass.async_block_till_done() + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_component_mock = MagicMock( + return_value=hass.data["mqtt"].client, + wraps=hass.data["mqtt"].client, + ) + mqtt_component_mock._mqttc = mqtt_client_mock + + hass.data["mqtt"].client = mqtt_component_mock + mqtt_mock = hass.data["mqtt"].client + mqtt_mock.reset_mock() + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + mqtt_client_mock.reset_mock() + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0): + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + mqtt_client_mock.on_connect(None, None, 0, 0) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # We wait until we receive a birth message + await asyncio.wait_for(birth.wait(), 1) + # Assert we already have subscribed at the client + # for new config payloads at the time we the birth message is received + assert ("homeassistant/+/+/config", 0) in help_all_subscribe_calls( + mqtt_client_mock + ) + assert ("homeassistant/+/+/+/config", 0) in help_all_subscribe_calls( + mqtt_client_mock + ) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + @pytest.mark.parametrize( "mqtt_config_entry_data", [ @@ -3022,14 +3121,16 @@ async def test_debug_info_multiple_devices( for dev in devices: data = json.dumps(dev["config"]) domain = dev["domain"] - id = dev["config"]["device"]["identifiers"][0] - async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data) + device_id = dev["config"]["device"]["identifiers"][0] + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/{device_id}/config", data + ) await hass.async_block_till_done() for dev in devices: domain = dev["domain"] - id = dev["config"]["device"]["identifiers"][0] - device = device_registry.async_get_device(identifiers={("mqtt", id)}) + device_id = dev["config"]["device"]["identifiers"][0] + device = device_registry.async_get_device(identifiers={("mqtt", device_id)}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -3047,7 +3148,7 @@ async def test_debug_info_multiple_devices( assert len(debug_info_data["triggers"]) == 1 discovery_data = debug_info_data["triggers"][0]["discovery_data"] - assert discovery_data["topic"] == f"homeassistant/{domain}/{id}/config" + assert discovery_data["topic"] == f"homeassistant/{domain}/{device_id}/config" assert discovery_data["payload"] == dev["config"] @@ -3105,8 +3206,10 @@ async def test_debug_info_multiple_entities_triggers( data = json.dumps(c["config"]) domain = c["domain"] # Use topic as discovery_id - id = c["config"].get("topic", c["config"].get("state_topic")) - async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data) + discovery_id = c["config"].get("topic", c["config"].get("state_topic")) + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/{discovery_id}/config", data + ) await hass.async_block_till_done() device_id = config[0]["config"]["device"]["identifiers"][0] @@ -3120,7 +3223,7 @@ async def test_debug_info_multiple_entities_triggers( # Test we get debug info for each entity and trigger domain = c["domain"] # Use topic as discovery_id - id = c["config"].get("topic", c["config"].get("state_topic")) + discovery_id = c["config"].get("topic", c["config"].get("state_topic")) if c["domain"] != "device_automation": discovery_data = [e["discovery_data"] for e in debug_info_data["entities"]] @@ -3132,7 +3235,7 @@ async def test_debug_info_multiple_entities_triggers( discovery_data = [e["discovery_data"] for e in debug_info_data["triggers"]] assert { - "topic": f"homeassistant/{domain}/{id}/config", + "topic": f"homeassistant/{domain}/{discovery_id}/config", "payload": c["config"], } in discovery_data @@ -4151,3 +4254,179 @@ async def test_multi_platform_discovery( ) is not None ) + + +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_auto_reconnect( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reconnection is automatically done.""" + mqtt_mock = await mqtt_mock_entry() + await hass.async_block_till_done() + assert mqtt_mock.connected is True + mqtt_client_mock.reconnect.reset_mock() + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + mqtt_client_mock.reconnect.side_effect = OSError("foo") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 1 + assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text + + mqtt_client_mock.reconnect.side_effect = None + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + # Should not reconnect after stop + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_server_sock_connect_and_disconnect( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, + calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_mock = await mqtt_mock_entry() + await hass.async_block_till_done() + assert mqtt_mock.connected is True + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + server.close() # mock the server closing the connection on us + + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) + mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) + await hass.async_block_till_done() + unsub() + + # Should have failed + assert len(calls) == 0 + + +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_client_sock_failure_after_connect( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, + calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_mock = await mqtt_mock_entry() + # Fake that the client is connected + mqtt_mock().connected = True + await hass.async_block_till_done() + assert mqtt_mock.connected is True + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + mqtt_client_mock.loop_write.side_effect = OSError("foo") + client.close() # close the client socket out from under the client + + assert mqtt_mock.connected is True + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + unsub() + # Should have failed + assert len(calls) == 0 + + +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_loop_write_failure( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_mock = await mqtt_mock_entry() + await hass.async_block_till_done() + assert mqtt_mock.connected is True + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + + # Fill up the outgoing buffer to ensure that loop_write + # and loop_read are called that next time control is + # returned to the event loop + try: + for _ in range(1000): + server.send(b"long" * 100) + except BlockingIOError: + pass + + server.close() + # Once for the reader callback + await hass.async_block_till_done() + # Another for the writer callback + await hass.async_block_till_done() + # Final for the disconnect callback + await hass.async_block_till_done() + + assert "Disconnected from MQTT server mock-broker:1883 (7)" in caplog.text diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index ff1b308ef70..739240a352c 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -236,7 +236,7 @@ async def test_warning_if_color_mode_flags_are_used( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, - color_modes: tuple[str,], + color_modes: tuple[str, ...], ) -> None: """Test warnings deprecated config keys without supported color modes defined.""" with patch( @@ -278,7 +278,7 @@ async def test_warning_on_discovery_if_color_mode_flags_are_used( mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, config: dict[str, Any], - color_modes: tuple[str,], + color_modes: tuple[str, ...], ) -> None: """Test warnings deprecated config keys with discovery.""" with patch( diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py new file mode 100644 index 00000000000..bc833b79eb0 --- /dev/null +++ b/tests/components/mqtt/test_notify.py @@ -0,0 +1,474 @@ +"""The tests for the MQTT notify platform.""" + +import copy +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components import mqtt, notify +from homeassistant.components.notify import ATTR_MESSAGE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_publishing_with_custom_encoding, + help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_unload_config_entry_with_platform, + help_test_update_with_json_attrs_bad_json, + help_test_update_with_json_attrs_not_dict, +) + +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient + +DEFAULT_CONFIG = { + mqtt.DOMAIN: {notify.DOMAIN: {"name": "test", "command_topic": "test-topic"}} +} + + +@pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + notify.DOMAIN: { + "command_topic": "command-topic", + "name": "test", + "object_id": "test_notify", + "qos": "2", + } + } + } + ], +) +async def test_sending_mqtt_commands( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the sending MQTT commands.""" + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get("notify.test_notify") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "Beer message", ATTR_ENTITY_ID: "notify.test_notify"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "Beer message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("notify.test_notify") + assert state.state == "2021-11-08T13:31:44+00:00" + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + notify.DOMAIN: { + "command_topic": "command-topic", + "command_template": '{ "{{ entity_id }}": "{{ value }}" }', + "name": "test", + } + } + } + ], +) +async def test_command_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the sending of MQTT commands through a command template.""" + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get("notify.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "Beer message", ATTR_ENTITY_ID: "notify.test"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{ "notify.test": "Beer message" }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_when_connection_lost( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock_entry, notify.DOMAIN + ) + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_without_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG, True + ) + + +async def test_custom_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, + mqtt_mock_entry, + notify.DOMAIN, + DEFAULT_CONFIG, + True, + ) + + +async def test_setting_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG, None + ) + + +async def test_setting_attribute_with_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_discovery_update_attr( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + DEFAULT_CONFIG, + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + notify.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + } + ], +) +async def test_unique_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test unique id option only creates one notify entity per unique_id.""" + await help_test_unique_id(hass, mqtt_mock_entry, notify.DOMAIN) + + +async def test_discovery_removal_notify( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removal of discovered notify.""" + data = '{ "name": "test", "command_topic": "test_topic" }' + await help_test_discovery_removal( + hass, mqtt_mock_entry, caplog, notify.DOMAIN, data + ) + + +async def test_discovery_update_notify( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered notify.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][notify.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][notify.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + + await help_test_discovery_update( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + config1, + config2, + ) + + +async def test_discovery_update_unchanged_notify( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered notify.""" + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "homeassistant.components.mqtt.notify.MqttNotify.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + data1, + discovery_update, + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic" }' + await help_test_discovery_broken( + hass, mqtt_mock_entry, caplog, notify.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT notify device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT notify device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, + mqtt_mock_entry, + notify.DOMAIN, + DEFAULT_CONFIG, + notify.SERVICE_SEND_MESSAGE, + command_topic="test-topic", + command_payload="Milk", + state_topic=None, + service_parameters={"message": "Milk"}, + ) + + +@pytest.mark.parametrize( + ("service", "topic", "parameters", "payload", "template"), + [ + ( + notify.SERVICE_SEND_MESSAGE, + "command_topic", + {"message": "Beer test"}, + "Beer test", + "command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + service: str, + topic: str, + parameters: dict[str, Any], + payload: str, + template: str | None, +) -> None: + """Test publishing MQTT payload with different encoding.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock_entry, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) + + +async def test_reloadable( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test reloading the MQTT platform.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable(hass, mqtt_client_mock, domain, config) + + +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) +async def test_setup_manual_entity_from_yaml( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setup manual configured MQTT entity.""" + await mqtt_mock_entry() + platform = notify.DOMAIN + assert hass.states.get(f"{platform}.test") + + +async def test_unload_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test unloading the config entry.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry, domain, config + ) diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 90a39bfd4fb..ceb9207e0c2 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -4,7 +4,7 @@ from unittest.mock import ANY import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index b07dfc1f642..c485e8a9c27 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -162,7 +162,7 @@ async def test_waiting_for_client_not_loaded( for _ in range(4): hass.async_create_task(_async_just_in_time_subscribe()) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert await hass.config_entries.async_setup(entry.entry_id) assert len(unsubs) == 4 for unsub in unsubs: @@ -182,7 +182,7 @@ async def test_waiting_for_client_loaded( unsub = await mqtt.async_subscribe(hass, "test_topic", lambda msg: None) entry = hass.config_entries.async_entries(mqtt.DATA_MQTT)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await _async_just_in_time_subscribe() @@ -209,13 +209,13 @@ async def test_waiting_for_client_entry_fails( assert not await mqtt.async_wait_for_mqtt_client(hass) hass.async_create_task(_async_just_in_time_subscribe()) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.mqtt.async_setup_entry", side_effect=Exception, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_waiting_for_client_setup_fails( @@ -237,12 +237,12 @@ async def test_waiting_for_client_setup_fails( assert not await mqtt.async_wait_for_mqtt_client(hass) hass.async_create_task(_async_just_in_time_subscribe()) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # Simulate MQTT setup fails before the client would become available mqtt_client_mock.connect.side_effect = Exception assert not await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR @patch("homeassistant.components.mqtt.util.AVAILABILITY_TIMEOUT", 0.01) @@ -260,7 +260,7 @@ async def test_waiting_for_client_timeout( ) entry.add_to_hass(hass) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # returns False after timeout assert not await mqtt.async_wait_for_mqtt_client(hass) @@ -284,7 +284,7 @@ async def test_waiting_for_client_with_disabled_entry( entry.entry_id, ConfigEntryDisabler.USER ) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # returns False because entry is disabled assert not await mqtt.async_wait_for_mqtt_client(hass) diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 24b4a83c425..90034382fc8 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -111,7 +111,7 @@ async def test_state_changed_event_sends_message( "last_updated": now.isoformat(), "state": "on", } - event["event_data"] = {"new_state": new_state, "entity_id": e_id} + event["event_data"] = {"new_state": new_state, "entity_id": e_id, "old_state": None} # Verify that the message received was that expected result = json.loads(msg) diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index bc1890f08fa..e6fe7db3b8e 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import patch import pytest +from homeassistant.components import sensor from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS -import homeassistant.components.sensor as sensor from homeassistant.const import ( CONF_DEVICE_ID, CONF_NAME, diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index e1e6570fa67..dbf90be3416 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with ( @@ -36,7 +36,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Mullvad VPN" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -50,8 +50,8 @@ async def test_form_user_only_once(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" async def test_connection_error(hass: HomeAssistant) -> None: @@ -71,7 +71,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -92,5 +92,5 @@ async def test_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py index f667671da74..07bcfe66f02 100644 --- a/tests/components/mutesync/test_config_flow.py +++ b/tests/components/mutesync/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.mutesync.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -16,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -37,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -74,5 +75,5 @@ async def test_form_error( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index bcf852e1368..e18043fda1f 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -116,7 +116,7 @@ def transport_write(transport: MagicMock) -> MagicMock: @pytest.fixture(name="serial_entry") async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" - entry = MockConfigEntry( + return MockConfigEntry( domain=DOMAIN, data={ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, @@ -125,7 +125,6 @@ async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: CONF_BAUD_RATE: DEFAULT_BAUD_RATE, }, ) - return entry @pytest.fixture(name="config_entry") @@ -219,8 +218,7 @@ def cover_node_binary( ) -> Sensor: """Load the cover child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(cover_node_binary_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="cover_node_percentage_state", scope="session") @@ -235,8 +233,7 @@ def cover_node_percentage( ) -> Sensor: """Load the cover child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(cover_node_percentage_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="door_sensor_state", scope="session") @@ -249,8 +246,7 @@ def door_sensor_state_fixture() -> dict: def door_sensor(gateway_nodes: dict[int, Sensor], door_sensor_state: dict) -> Sensor: """Load the door sensor.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(door_sensor_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="gps_sensor_state", scope="session") @@ -263,8 +259,7 @@ def gps_sensor_state_fixture() -> dict: def gps_sensor(gateway_nodes: dict[int, Sensor], gps_sensor_state: dict) -> Sensor: """Load the gps sensor.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(gps_sensor_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="dimmer_node_state", scope="session") @@ -277,8 +272,7 @@ def dimmer_node_state_fixture() -> dict: def dimmer_node(gateway_nodes: dict[int, Sensor], dimmer_node_state: dict) -> Sensor: """Load the dimmer child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(dimmer_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="hvac_node_auto_state", scope="session") @@ -293,8 +287,7 @@ def hvac_node_auto( ) -> Sensor: """Load the hvac auto child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(hvac_node_auto_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="hvac_node_cool_state", scope="session") @@ -309,8 +302,7 @@ def hvac_node_cool( ) -> Sensor: """Load the hvac cool child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(hvac_node_cool_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="hvac_node_heat_state", scope="session") @@ -325,8 +317,7 @@ def hvac_node_heat( ) -> Sensor: """Load the hvac heat child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(hvac_node_heat_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="power_sensor_state", scope="session") @@ -339,8 +330,7 @@ def power_sensor_state_fixture() -> dict: def power_sensor(gateway_nodes: dict[int, Sensor], power_sensor_state: dict) -> Sensor: """Load the power sensor.""" nodes = update_gateway_nodes(gateway_nodes, power_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="rgb_node_state", scope="session") @@ -353,8 +343,7 @@ def rgb_node_state_fixture() -> dict: def rgb_node(gateway_nodes: dict[int, Sensor], rgb_node_state: dict) -> Sensor: """Load the rgb child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(rgb_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="rgbw_node_state", scope="session") @@ -367,8 +356,7 @@ def rgbw_node_state_fixture() -> dict: def rgbw_node(gateway_nodes: dict[int, Sensor], rgbw_node_state: dict) -> Sensor: """Load the rgbw child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(rgbw_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="energy_sensor_state", scope="session") @@ -383,8 +371,7 @@ def energy_sensor( ) -> Sensor: """Load the energy sensor.""" nodes = update_gateway_nodes(gateway_nodes, energy_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="sound_sensor_state", scope="session") @@ -397,8 +384,7 @@ def sound_sensor_state_fixture() -> dict: def sound_sensor(gateway_nodes: dict[int, Sensor], sound_sensor_state: dict) -> Sensor: """Load the sound sensor.""" nodes = update_gateway_nodes(gateway_nodes, sound_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="distance_sensor_state", scope="session") @@ -413,8 +399,7 @@ def distance_sensor( ) -> Sensor: """Load the distance sensor.""" nodes = update_gateway_nodes(gateway_nodes, distance_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="ir_transceiver_state", scope="session") @@ -429,8 +414,7 @@ def ir_transceiver( ) -> Sensor: """Load the ir transceiver child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(ir_transceiver_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="relay_node_state", scope="session") @@ -443,8 +427,7 @@ def relay_node_state_fixture() -> dict: def relay_node(gateway_nodes: dict[int, Sensor], relay_node_state: dict) -> Sensor: """Load the relay child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(relay_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="temperature_sensor_state", scope="session") @@ -459,8 +442,7 @@ def temperature_sensor( ) -> Sensor: """Load the temperature sensor.""" nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="text_node_state", scope="session") @@ -473,8 +455,7 @@ def text_node_state_fixture() -> dict: def text_node(gateway_nodes: dict[int, Sensor], text_node_state: dict) -> Sensor: """Load the text child node.""" nodes = update_gateway_nodes(gateway_nodes, text_node_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="battery_sensor_state", scope="session") @@ -489,5 +470,4 @@ def battery_sensor( ) -> Sensor: """Load the battery sensor.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(battery_sensor_state)) - node = nodes[1] - return node + return nodes[1] diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index f532d09c6bf..c75f8743cde 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -44,13 +44,13 @@ async def get_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": GATEWAY_TYPE_TO_STEP[gateway_type]} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == expected_step_id return result @@ -78,7 +78,7 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: if "errors" in result: assert not result["errors"] - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "mqtt" assert result["data"] == { CONF_DEVICE: "mqtt", @@ -96,7 +96,7 @@ async def test_missing_mqtt(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -104,7 +104,7 @@ async def test_missing_mqtt(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "mqtt_required" @@ -139,7 +139,7 @@ async def test_config_serial(hass: HomeAssistant) -> None: if "errors" in result: assert not result["errors"] - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyACM0" assert result["data"] == { CONF_DEVICE: "/dev/ttyACM0", @@ -177,7 +177,7 @@ async def test_config_tcp(hass: HomeAssistant) -> None: if "errors" in result: assert not result["errors"] - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "127.0.0.1" assert result["data"] == { CONF_DEVICE: "127.0.0.1", @@ -213,7 +213,7 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert "errors" in result errors = result["errors"] assert errors @@ -374,7 +374,7 @@ async def test_config_invalid( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert "errors" in result errors = result["errors"] assert errors diff --git a/tests/components/mystrom/test_config_flow.py b/tests/components/mystrom/test_config_flow.py index b64b4edf547..d0b3603b211 100644 --- a/tests/components/mystrom/test_config_flow.py +++ b/tests/components/mystrom/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form_combined(hass: HomeAssistant, mock_setup_entry: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -38,7 +38,7 @@ async def test_form_combined(hass: HomeAssistant, mock_setup_entry: AsyncMock) - ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myStrom Device" assert result2["data"] == {"host": "1.1.1.1"} @@ -50,7 +50,7 @@ async def test_form_duplicates( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_form_duplicates( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_session.assert_called_once() @@ -78,7 +78,7 @@ async def test_wong_answer_from_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} with patch( @@ -93,7 +93,7 @@ async def test_wong_answer_from_device(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -108,6 +108,6 @@ async def test_wong_answer_from_device(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myStrom Device" assert result2["data"] == {"host": "1.1.1.1"} diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 0304a0eb270..f22665efb6b 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -113,7 +113,7 @@ async def test_init_of_unknown_bulb( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_init_of_unknown_device( @@ -127,7 +127,7 @@ async def test_init_of_unknown_device( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_init_cannot_connect_because_of_device_info( @@ -145,7 +145,7 @@ async def test_init_cannot_connect_because_of_device_info( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_init_cannot_connect_because_of_get_state( @@ -168,4 +168,4 @@ async def test_init_cannot_connect_because_of_get_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/myuplink/test_config_flow.py b/tests/components/myuplink/test_config_flow.py index 1a4656bed73..7c5ae2c8657 100644 --- a/tests/components/myuplink/test_config_flow.py +++ b/tests/components/myuplink/test_config_flow.py @@ -155,7 +155,7 @@ async def test_flow_reauth( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/myuplink/test_init.py b/tests/components/myuplink/test_init.py index 328dc55d4ad..421eb9b59c2 100644 --- a/tests/components/myuplink/test_init.py +++ b/tests/components/myuplink/test_init.py @@ -26,12 +26,12 @@ async def test_load_unload_entry( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index 0484fc12bd6..9b254de452c 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -4,44 +4,13 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.nam.const import DOMAIN -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_object_fixture INCOMPLETE_NAM_DATA = { "software_version": "NAMF-2020-36", "sensordatavalues": [], } -nam_data = { - "software_version": "NAMF-2020-36", - "uptime": "456987", - "sensordatavalues": [ - {"value_type": "PMS_P0", "value": "6.00"}, - {"value_type": "PMS_P1", "value": "10.00"}, - {"value_type": "PMS_P2", "value": "11.00"}, - {"value_type": "SDS_P1", "value": "18.65"}, - {"value_type": "SDS_P2", "value": "11.03"}, - {"value_type": "SPS30_P0", "value": "31.23"}, - {"value_type": "SPS30_P1", "value": "21.23"}, - {"value_type": "SPS30_P2", "value": "34.32"}, - {"value_type": "SPS30_P4", "value": "24.72"}, - {"value_type": "conc_co2_ppm", "value": "865"}, - {"value_type": "BME280_temperature", "value": "7.56"}, - {"value_type": "BME280_humidity", "value": "45.69"}, - {"value_type": "BME280_pressure", "value": "101101.17"}, - {"value_type": "BMP_temperature", "value": "7.56"}, - {"value_type": "BMP_pressure", "value": "103201.18"}, - {"value_type": "BMP280_temperature", "value": "5.56"}, - {"value_type": "BMP280_pressure", "value": "102201.18"}, - {"value_type": "SHT3X_temperature", "value": "6.28"}, - {"value_type": "SHT3X_humidity", "value": "34.69"}, - {"value_type": "humidity", "value": "46.23"}, - {"value_type": "temperature", "value": "6.26"}, - {"value_type": "HECA_temperature", "value": "7.95"}, - {"value_type": "HECA_humidity", "value": "49.97"}, - {"value_type": "signal", "value": "-72"}, - ], -} - async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: """Set up the Nettigo Air Monitor integration in Home Assistant.""" @@ -52,6 +21,8 @@ async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: data={"host": "10.10.2.3"}, ) + nam_data = load_json_object_fixture("nam/nam_data.json") + if not co2_sensor: # Remove conc_co2_ppm value nam_data["sensordatavalues"].pop(6) diff --git a/tests/components/nam/fixtures/diagnostics_data.json b/tests/components/nam/fixtures/diagnostics_data.json deleted file mode 100644 index a384e8cd386..00000000000 --- a/tests/components/nam/fixtures/diagnostics_data.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "bme280_humidity": 45.7, - "bme280_pressure": 1011.012, - "bme280_temperature": 7.6, - "bmp180_pressure": 1032.012, - "bmp180_temperature": 7.6, - "bmp280_pressure": 1022.012, - "bmp280_temperature": 5.6, - "dht22_humidity": 46.2, - "dht22_temperature": 6.3, - "heca_humidity": 50.0, - "heca_temperature": 8.0, - "mhz14a_carbon_dioxide": 865.0, - "pms_caqi": 19, - "pms_caqi_level": "very_low", - "pms_p0": 6.0, - "pms_p1": 10.0, - "pms_p2": 11.0, - "sds011_caqi": 19, - "sds011_caqi_level": "very_low", - "sds011_p1": 18.6, - "sds011_p2": 11.0, - "sht3x_humidity": 34.7, - "sht3x_temperature": 6.3, - "signal": -72.0, - "sps30_caqi": 54, - "sps30_caqi_level": "medium", - "sps30_p0": 31.2, - "sps30_p1": 21.2, - "sps30_p2": 34.3, - "sps30_p4": 24.7, - "uptime": 456987 -} diff --git a/tests/components/nam/fixtures/nam_data.json b/tests/components/nam/fixtures/nam_data.json new file mode 100644 index 00000000000..93a33d4a552 --- /dev/null +++ b/tests/components/nam/fixtures/nam_data.json @@ -0,0 +1,30 @@ +{ + "software_version": "NAMF-2020-36", + "uptime": "456987", + "sensordatavalues": [ + { "value_type": "PMS_P0", "value": "6.00" }, + { "value_type": "PMS_P1", "value": "10.00" }, + { "value_type": "PMS_P2", "value": "11.00" }, + { "value_type": "SDS_P1", "value": "18.65" }, + { "value_type": "SDS_P2", "value": "11.03" }, + { "value_type": "SPS30_P0", "value": "31.23" }, + { "value_type": "SPS30_P1", "value": "21.23" }, + { "value_type": "SPS30_P2", "value": "34.32" }, + { "value_type": "SPS30_P4", "value": "24.72" }, + { "value_type": "conc_co2_ppm", "value": "865" }, + { "value_type": "BME280_temperature", "value": "7.56" }, + { "value_type": "BME280_humidity", "value": "45.69" }, + { "value_type": "BME280_pressure", "value": "101101.17" }, + { "value_type": "BMP_temperature", "value": "7.56" }, + { "value_type": "BMP_pressure", "value": "103201.18" }, + { "value_type": "BMP280_temperature", "value": "5.56" }, + { "value_type": "BMP280_pressure", "value": "102201.18" }, + { "value_type": "SHT3X_temperature", "value": "6.28" }, + { "value_type": "SHT3X_humidity", "value": "34.69" }, + { "value_type": "humidity", "value": "46.23" }, + { "value_type": "temperature", "value": "6.26" }, + { "value_type": "HECA_temperature", "value": "7.95" }, + { "value_type": "HECA_humidity", "value": "49.97" }, + { "value_type": "signal", "value": "-72" } + ] +} diff --git a/tests/components/nam/snapshots/test_diagnostics.ambr b/tests/components/nam/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..2ebc0246090 --- /dev/null +++ b/tests/components/nam/snapshots/test_diagnostics.ambr @@ -0,0 +1,41 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'bme280_humidity': 45.7, + 'bme280_pressure': 1011.012, + 'bme280_temperature': 7.6, + 'bmp180_pressure': 1032.012, + 'bmp180_temperature': 7.6, + 'bmp280_pressure': 1022.012, + 'bmp280_temperature': 5.6, + 'dht22_humidity': 46.2, + 'dht22_temperature': 6.3, + 'heca_humidity': 50.0, + 'heca_temperature': 8.0, + 'mhz14a_carbon_dioxide': 865.0, + 'pms_caqi': 19, + 'pms_caqi_level': 'very_low', + 'pms_p0': 6.0, + 'pms_p1': 10.0, + 'pms_p2': 11.0, + 'sds011_caqi': 19, + 'sds011_caqi_level': 'very_low', + 'sds011_p1': 18.6, + 'sds011_p2': 11.0, + 'sht3x_humidity': 34.7, + 'sht3x_temperature': 6.3, + 'signal': -72.0, + 'sps30_caqi': 54, + 'sps30_caqi_level': 'medium', + 'sps30_p0': 31.2, + 'sps30_p1': 21.2, + 'sps30_p2': 34.3, + 'sps30_p4': 24.7, + 'uptime': 456987, + }), + 'info': dict({ + 'host': '10.10.2.3', + }), + }) +# --- diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..bbc655ecbb6 --- /dev/null +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -0,0 +1,1714 @@ +# serializer version: 1 +# name: test_sensor[button.nettigo_air_monitor_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.nettigo_air_monitor_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[button.nettigo_air_monitor_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Nettigo Air Monitor Restart', + }), + 'context': , + 'entity_id': 'button.nettigo_air_monitor_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_humidity-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.nettigo_air_monitor_bme280_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'BME280 humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bme280_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor BME280 humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bme280_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.7', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_pressure-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.nettigo_air_monitor_bme280_pressure', + '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': 'BME280 pressure', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bme280_pressure', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Nettigo Air Monitor BME280 pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bme280_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1011.012', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_temperature-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.nettigo_air_monitor_bme280_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'BME280 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bme280_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor BME280 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bme280_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_pressure-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.nettigo_air_monitor_bmp180_pressure', + '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': 'BMP180 pressure', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp180_pressure', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp180_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Nettigo Air Monitor BMP180 pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp180_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1032.012', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_temperature-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.nettigo_air_monitor_bmp180_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'BMP180 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp180_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp180_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor BMP180 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp180_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_pressure-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.nettigo_air_monitor_bmp280_pressure', + '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': 'BMP280 pressure', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp280_pressure', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp280_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Nettigo Air Monitor BMP280 pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp280_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1022.012', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_temperature-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.nettigo_air_monitor_bmp280_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'BMP280 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp280_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp280_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor BMP280 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp280_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_humidity-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.nettigo_air_monitor_dht22_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHT22 humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dht22_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor DHT22 humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_dht22_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '46.2', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_temperature-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.nettigo_air_monitor_dht22_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHT22 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dht22_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor DHT22 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_dht22_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.3', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_humidity-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.nettigo_air_monitor_heca_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HECA humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heca_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-heca_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor HECA humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_heca_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_temperature-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.nettigo_air_monitor_heca_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HECA temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heca_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-heca_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor HECA temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_heca_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_last_restart-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.nettigo_air_monitor_last_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last restart', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_restart', + 'unique_id': 'aa:bb:cc:dd:ee:ff-uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_last_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Nettigo Air Monitor Last restart', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_last_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-04-15T05:03:33+00:00', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide-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.nettigo_air_monitor_mh_z14a_carbon_dioxide', + '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': 'MH-Z14A carbon dioxide', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mhz14a_carbon_dioxide', + 'unique_id': 'aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Nettigo Air Monitor MH-Z14A carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '865.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index-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.nettigo_air_monitor_pmsx003_common_air_quality_index', + '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': 'PMSx003 common air quality index', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_caqi', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_caqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor PMSx003 common air quality index', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_common_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PMSx003 common air quality index level', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_caqi_level', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_caqi_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Nettigo Air Monitor PMSx003 common air quality index level', + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'very_low', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-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.nettigo_air_monitor_pmsx003_pm1', + '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': 'PMSx003 PM1', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_pm1', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm1', + 'friendly_name': 'Nettigo Air Monitor PMSx003 PM1', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-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.nettigo_air_monitor_pmsx003_pm10', + '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': 'PMSx003 PM10', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_pm10', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Nettigo Air Monitor PMSx003 PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-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.nettigo_air_monitor_pmsx003_pm2_5', + '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': 'PMSx003 PM2.5', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_pm25', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Nettigo Air Monitor PMSx003 PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index-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.nettigo_air_monitor_sds011_common_air_quality_index', + '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': 'SDS011 common air quality index', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_caqi', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_caqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor SDS011 common air quality index', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_common_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_sds011_common_air_quality_index_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SDS011 common air quality index level', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_caqi_level', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_caqi_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Nettigo Air Monitor SDS011 common air quality index level', + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_common_air_quality_index_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'very_low', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-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.nettigo_air_monitor_sds011_pm10', + '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': 'SDS011 PM10', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_pm10', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Nettigo Air Monitor SDS011 PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-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.nettigo_air_monitor_sds011_pm2_5', + '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': 'SDS011 PM2.5', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_pm25', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Nettigo Air Monitor SDS011 PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_humidity-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.nettigo_air_monitor_sht3x_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SHT3X humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sht3x_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor SHT3X humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sht3x_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.7', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_temperature-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.nettigo_air_monitor_sht3x_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SHT3X temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sht3x_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor SHT3X temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sht3x_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.3', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_signal_strength-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.nettigo_air_monitor_signal_strength', + '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': 'Signal strength', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-signal', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Nettigo Air Monitor Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-72.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index-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.nettigo_air_monitor_sps30_common_air_quality_index', + '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': 'SPS30 common air quality index', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_caqi', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_caqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor SPS30 common air quality index', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_common_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_sps30_common_air_quality_index_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SPS30 common air quality index level', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_caqi_level', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_caqi_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Nettigo Air Monitor SPS30 common air quality index level', + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_common_air_quality_index_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-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.nettigo_air_monitor_sps30_pm1', + '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': 'SPS30 PM1', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm1', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm1', + 'friendly_name': 'Nettigo Air Monitor SPS30 PM1', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.2', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-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.nettigo_air_monitor_sps30_pm10', + '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': 'SPS30 PM10', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm10', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Nettigo Air Monitor SPS30 PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.2', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-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.nettigo_air_monitor_sps30_pm2_5', + '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': 'SPS30 PM2.5', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm25', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Nettigo Air Monitor SPS30 PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.3', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-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.nettigo_air_monitor_sps30_pm4', + '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': None, + 'original_icon': None, + 'original_name': 'SPS30 PM4', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm4', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor SPS30 PM4', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.7', + }) +# --- diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 71bf3cf1525..5dff9855988 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -6,11 +6,11 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError, AuthFailedError, CannotGetMacError import pytest -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -34,7 +34,7 @@ async def test_form_create_entry_without_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -57,7 +57,7 @@ async def test_form_create_entry_without_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert len(mock_setup_entry.mock_calls) == 1 @@ -68,7 +68,7 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -90,7 +90,7 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" result = await hass.config_entries.flow.async_configure( @@ -99,7 +99,7 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -133,7 +133,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -141,7 +141,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -165,7 +165,7 @@ async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -173,7 +173,7 @@ async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -205,7 +205,7 @@ async def test_form_with_auth_errors(hass: HomeAssistant, error) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" with patch( @@ -262,7 +262,7 @@ async def test_form_abort(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "device_unsupported" @@ -292,7 +292,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -322,7 +322,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True @@ -337,7 +337,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"] == {"host": "10.10.2.3"} assert len(mock_setup_entry.mock_calls) == 1 @@ -366,7 +366,7 @@ async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" @@ -390,7 +390,7 @@ async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -411,7 +411,7 @@ async def test_zeroconf_host_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -435,5 +435,5 @@ async def test_zeroconf_errors(hass: HomeAssistant, error) -> None: context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/nam/test_diagnostics.py b/tests/components/nam/test_diagnostics.py index 9d13121392f..7ed49a37e0a 100644 --- a/tests/components/nam/test_diagnostics.py +++ b/tests/components/nam/test_diagnostics.py @@ -1,25 +1,23 @@ """Test NAM diagnostics.""" -import json +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass) - diagnostics_data = json.loads(load_fixture("diagnostics_data.json", "nam")) - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result["info"] == {"host": "10.10.2.3"} - assert result["data"] == diagnostics_data + assert result == snapshot diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index c88a34ae497..2b307b4b02a 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -3,27 +3,18 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory from nettigo_air_monitor import ApiError +from syrupy import SnapshotAssertion from homeassistant.components.nam.const import DOMAIN -from homeassistant.components.sensor import ( - ATTR_OPTIONS, - ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorStateClass, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, - PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNAVAILABLE, - UnitOfPressure, + Platform, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -31,447 +22,30 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import INCOMPLETE_NAM_DATA, init_integration, nam_data +from . import INCOMPLETE_NAM_DATA, init_integration -from tests.common import async_fire_time_changed +from tests.common import ( + async_fire_time_changed, + load_json_object_fixture, + snapshot_platform, +) -async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_sensor( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, +) -> None: """Test states of the air_quality.""" - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aa:bb:cc:dd:ee:ff-signal", - suggested_object_id="nettigo_air_monitor_signal_strength", - disabled_by=None, - ) + hass.config.set_time_zone("UTC") + freezer.move_to("2024-04-20 12:00:00+00:00") - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aa:bb:cc:dd:ee:ff-uptime", - suggested_object_id="nettigo_air_monitor_uptime", - disabled_by=None, - ) + with patch("homeassistant.components.nam.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - # Patch return value from utcnow, with offset to make sure the patch is correct - now = utcnow() - timedelta(hours=1) - with patch("homeassistant.components.nam.sensor.utcnow", return_value=now): - await init_integration(hass) - - state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity") - assert state - assert state.state == "45.7" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bme280_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") - assert state - assert state.state == "7.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_bme280_pressure") - assert state - assert state.state == "1011.012" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_pressure" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp180_temperature") - assert state - assert state.state == "7.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp180_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp180_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp180_pressure") - assert state - assert state.state == "1032.012" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp180_pressure") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp180_pressure" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature") - assert state - assert state.state == "5.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp280_pressure") - assert state - assert state.state == "1022.012" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_pressure" - - state = hass.states.get("sensor.nettigo_air_monitor_sht3x_humidity") - assert state - assert state.state == "34.7" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_sht3x_temperature") - assert state - assert state.state == "6.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_dht22_humidity") - assert state - assert state.state == "46.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature") - assert state - assert state.state == "6.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity") - assert state - assert state.state == "50.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_heca_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") - assert state - assert state.state == "8.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_heca_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_signal_strength") - assert state - assert state.state == "-72.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == SIGNAL_STRENGTH_DECIBELS_MILLIWATT - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_signal_strength") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" - - state = hass.states.get("sensor.nettigo_air_monitor_uptime") - assert state - assert ( - state.state - == (now - timedelta(seconds=456987)).replace(microsecond=0).isoformat() - ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_uptime") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" - - state = hass.states.get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level" - ) - assert state - assert state.state == "very_low" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_low", - "low", - "medium", - "high", - "very_high", - ] - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi_level" - assert entry.translation_key == "pmsx003_caqi_level" - - state = hass.states.get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index" - ) - assert state - assert state.state == "19" - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi" - - state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_pm10") - assert state - assert state.state == "10.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_pmsx003_pm10") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p1" - - state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_pm2_5") - assert state - assert state.state == "11.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_pmsx003_pm2_5") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p2" - - state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_pm1") - assert state - assert state.state == "6.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_pmsx003_pm1") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p0" - - state = hass.states.get("sensor.nettigo_air_monitor_sds011_pm10") - assert state - assert state.state == "18.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sds011_pm10") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" - - state = hass.states.get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index" - ) - assert state - assert state.state == "19" - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" - - state = hass.states.get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index_level" - ) - assert state - assert state.state == "very_low" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_low", - "low", - "medium", - "high", - "very_high", - ] - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index_level" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" - assert entry.translation_key == "sds011_caqi_level" - - state = hass.states.get("sensor.nettigo_air_monitor_sds011_pm2_5") - assert state - assert state.state == "11.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sds011_pm2_5") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_common_air_quality_index") - assert state - assert state.state == "54" - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sps30_common_air_quality_index" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi" - - state = hass.states.get( - "sensor.nettigo_air_monitor_sps30_common_air_quality_index_level" - ) - assert state - assert state.state == "medium" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_low", - "low", - "medium", - "high", - "very_high", - ] - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sps30_common_air_quality_index_level" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi_level" - assert entry.translation_key == "sps30_caqi_level" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm1") - assert state - assert state.state == "31.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm1") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p0" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm10") - assert state - assert state.state == "21.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm10") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p1" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm2_5") - assert state - assert state.state == "34.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm2_5") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p2" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm4") - assert state - assert state.state == "24.7" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm4") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p4" - - state = hass.states.get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") - assert state - assert state.state == "865.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO2 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_MILLION - ) - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_sensor_disabled( @@ -524,6 +98,8 @@ async def test_incompleta_data_after_device_restart(hass: HomeAssistant) -> None async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when device causes an error.""" + nam_data = load_json_object_fixture("nam/nam_data.json") + await init_integration(hass) state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") @@ -566,6 +142,8 @@ async def test_availability(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeasasistant/update_entity.""" + nam_data = load_json_object_fixture("nam/nam_data.json") + await init_integration(hass) await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 5fe32c81eba..eaa1c60dcd4 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components import ssdp, zeroconf from homeassistant.components.nanoleaf.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -55,7 +56,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} assert not result2["last_step"] @@ -70,7 +71,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with patch( @@ -78,7 +79,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None side_effect=Unavailable, ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -106,7 +107,7 @@ async def test_user_error_setup_finish( CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -119,7 +120,7 @@ async def test_user_error_setup_finish( ), ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == reason @@ -139,7 +140,7 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" assert not result["last_step"] @@ -150,24 +151,24 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] is None assert result3["step_id"] == "link" result4 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result4["type"] == "form" + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": "not_allowing_new_tokens"} assert result4["step_id"] == "link" mock_nanoleaf.return_value.authorize.side_effect = None result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == "create_entry" + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == TEST_NAME assert result5["data"] == { CONF_HOST: TEST_HOST, @@ -192,7 +193,7 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} assert not result2["last_step"] @@ -212,14 +213,14 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None: mock_nanoleaf.return_value.authorize.side_effect = Exception() result4 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result4["type"] == "form" + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "link" assert result4["errors"] == {"base": "unknown"} mock_nanoleaf.return_value.authorize.side_effect = None mock_nanoleaf.return_value.get_info.side_effect = Exception() result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == "abort" + assert result5["type"] is FlowResultType.ABORT assert result5["reason"] == "unknown" @@ -257,7 +258,7 @@ async def test_discovery_link_unavailable( type=type_in_discovery_info, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" context = next( @@ -273,7 +274,7 @@ async def test_discovery_link_unavailable( side_effect=Unavailable, ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -305,11 +306,11 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_HOST] == TEST_HOST @@ -403,7 +404,7 @@ async def test_import_discovery_integration( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -451,14 +452,14 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_NAME assert result2["data"] == { CONF_HOST: TEST_HOST, diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 34b1f37f56f..132b23ef157 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -4,13 +4,15 @@ from unittest.mock import patch from pybotvac.neato import Neato -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.neato.const import NEATO_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -92,7 +94,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -118,7 +120,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -155,8 +157,8 @@ async def test_reauth( new_entry = hass.config_entries.async_get_entry("my_entry") - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" - assert new_entry.state == config_entries.ConfigEntryState.LOADED + assert new_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index e2cd536725f..70bc88b003f 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -148,7 +148,9 @@ class CreateDevice: self.data = {"traits": {}} def create( - self, raw_traits: dict[str, Any] = None, raw_data: dict[str, Any] = None + self, + raw_traits: dict[str, Any] | None = None, + raw_data: dict[str, Any] | None = None, ) -> None: """Create a new device with the specifeid traits.""" data = copy.deepcopy(self.data) diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index 3e0932c607d..fd07233fa8c 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -11,6 +11,7 @@ The tests below exercise both cases during integration setup. import time from unittest.mock import patch +from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import pytest from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES @@ -55,7 +56,7 @@ async def test_auth( async def async_new_subscriber( creds, subscription_name, event_loop, async_callback - ): + ) -> GoogleNestSubscriber | None: """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds @@ -123,7 +124,7 @@ async def test_auth_expired_token( async def async_new_subscriber( creds, subscription_name, event_loop, async_callback - ): + ) -> GoogleNestSubscriber | None: """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index b68173be201..33c611c9cfc 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -104,7 +104,7 @@ def webrtc_camera_device(create_device: CreateDevice) -> None: def make_motion_event( event_id: str = MOTION_EVENT_ID, event_session_id: str = EVENT_SESSION_ID, - timestamp: datetime.datetime = None, + timestamp: datetime.datetime | None = None, ) -> EventMessage: """Create an EventMessage for a motion event.""" if not timestamp: @@ -128,7 +128,7 @@ def make_motion_event( def make_stream_url_response( - expiration: datetime.datetime = None, token_num: int = 0 + expiration: datetime.datetime | None = None, token_num: int = 0 ) -> aiohttp.web.Response: """Make response for the API that generates a streaming url.""" if not expiration: diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 8d2a9e96d63..cef1f5e9a86 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.components import dhcp from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .common import ( @@ -67,13 +67,13 @@ class OAuthFixture: project_id: str = PROJECT_ID, ) -> None: """Invoke multiple steps in the app credentials based flow.""" - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await self.async_configure( result, {"cloud_project_id": CLOUD_PROJECT_ID} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await self.async_configure(result, {"project_id": project_id}) @@ -82,7 +82,7 @@ class OAuthFixture: async def async_oauth_web_flow(self, result: dict, project_id=PROJECT_ID) -> None: """Invoke the oauth flow for Web Auth with fake responses.""" state = self.create_state(result, WEB_REDIRECT_URL) - assert result["type"] == "external" + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == self.authorize_url( state, WEB_REDIRECT_URL, @@ -152,7 +152,7 @@ class OAuthFixture: ) async def async_finish_setup( - self, result: dict, user_input: dict = None + self, result: dict, user_input: dict | None = None ) -> ConfigEntry: """Finish the OAuth flow exchanging auth token for refresh token.""" with patch( @@ -175,7 +175,7 @@ class OAuthFixture: async def async_pubsub_flow(self, result: dict, cloud_project_id="") -> None: """Verify the pubsub creation step.""" # Render form with a link to get an auth token - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pubsub" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -246,14 +246,14 @@ async def test_config_flow_restart( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" # Change the values to show they are reflected below result = await oauth.async_configure( result, {"cloud_project_id": "new-cloud-project-id"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await oauth.async_configure(result, {"project_id": "new-project-id"}) @@ -291,17 +291,17 @@ async def test_config_flow_wrong_project_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" # Enter the cloud project id instead of device access project id (really we just check # they are the same value which is never correct) result = await oauth.async_configure(result, {"project_id": CLOUD_PROJECT_ID}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert "errors" in result assert "project_id" in result["errors"] assert result["errors"]["project_id"] == "wrong_project_id" @@ -351,7 +351,7 @@ async def test_config_flow_pubsub_configuration_error( mock_subscriber.create_subscription.side_effect = ConfigurationException result = await oauth.async_configure(result, {"code": "1234"}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert "errors" in result assert "cloud_project_id" in result["errors"] assert result["errors"]["cloud_project_id"] == "bad_project_id" @@ -372,7 +372,7 @@ async def test_config_flow_pubsub_subscriber_error( mock_subscriber.create_subscription.side_effect = SubscriberException() result = await oauth.async_configure(result, {"code": "1234"}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert "errors" in result assert "cloud_project_id" in result["errors"] assert result["errors"]["cloud_project_id"] == "subscriber_error" @@ -414,15 +414,15 @@ async def test_duplicate_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -522,7 +522,7 @@ async def test_pubsub_subscription_auth_failure( oauth.async_mock_refresh() result = await oauth.async_configure(result, {"code": "1234"}) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_access_token" @@ -693,11 +693,11 @@ async def test_dhcp_discovery( data=FAKE_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_cloud_project" result = await oauth.async_configure(result, {}) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -713,11 +713,11 @@ async def test_dhcp_discovery_with_creds( data=FAKE_DHCP_DATA, ) await hass.async_block_till_done() - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) @@ -775,5 +775,5 @@ async def test_token_error( ) result = await oauth.async_configure(result, user_input=None) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error_reason diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 51cf4254614..44fb6bcf701 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -4,7 +4,7 @@ from google_nest_sdm.event import EventMessage import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index 3cac8649c9c..e77ba3bb7e1 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -206,7 +206,7 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None: assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_remove_entry( diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 4810c8e2ff5..def99633435 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -85,8 +85,7 @@ def frame_image_data(frame_i, total_frames): img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) - return img + return np.clip(img, 0, 255) @pytest.fixture @@ -296,7 +295,7 @@ async def test_integration_unloaded(hass: HomeAssistant, auth, setup_platform) - assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # No devices returned browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") @@ -1166,10 +1165,10 @@ async def test_media_store_persistence( await hass.async_block_till_done() # Unload the integration. - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED # Now rebuild the entire integration and verify that all persisted storage # can be re-loaded from disk. diff --git a/tests/components/netatmo/snapshots/test_binary_sensor.ambr b/tests/components/netatmo/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6a90b4dd77a --- /dev/null +++ b/tests/components/netatmo/snapshots/test_binary_sensor.ambr @@ -0,0 +1,541 @@ +# serializer version: 1 +# name: test_entity[binary_sensor.baby_bedroom_connectivity-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.baby_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:68:92-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.baby_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Baby Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.baby_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.bedroom_connectivity-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.bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:69:0c-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.kitchen_connectivity-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.kitchen_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:25:cf:a8-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.kitchen_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Kitchen Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.kitchen_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.livingroom_connectivity-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.livingroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:65:14-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.livingroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Livingroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.livingroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.parents_bedroom_connectivity-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.parents_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:3e:c5:46-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.parents_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Parents Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.parents_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_bathroom_connectivity-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.villa_bathroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:7e:18-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_bathroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Bathroom Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_bathroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_bedroom_connectivity-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.villa_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:44:92-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Bedroom Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_connectivity-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.villa_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:bb:26-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Connectivity', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'binary_sensor.villa_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_garden_connectivity-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.villa_garden_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:03:1b:e4-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_garden_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Garden Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_garden_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_outdoor_connectivity-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.villa_outdoor_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:1c:42-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_outdoor_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Outdoor Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_outdoor_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.villa_rain_connectivity-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.villa_rain_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:c1:ea-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_rain_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Rain Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_rain_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/netatmo/snapshots/test_climate.ambr b/tests/components/netatmo/snapshots/test_climate.ambr index 327595e90a5..b9a92882b9e 100644 --- a/tests/components/netatmo/snapshots/test_climate.ambr +++ b/tests/components/netatmo/snapshots/test_climate.ambr @@ -26,7 +26,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.bureau', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -37,7 +37,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Bureau', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -101,7 +101,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.cocina', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -112,7 +112,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cocina', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -182,7 +182,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.corridor', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -193,7 +193,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Corridor', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -262,7 +262,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.entrada', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -273,7 +273,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Entrada', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -344,7 +344,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.livingroom', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -355,7 +355,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Livingroom', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , diff --git a/tests/components/netatmo/snapshots/test_cover.ambr b/tests/components/netatmo/snapshots/test_cover.ambr index e907985ab39..7ea016f5ae8 100644 --- a/tests/components/netatmo/snapshots/test_cover.ambr +++ b/tests/components/netatmo/snapshots/test_cover.ambr @@ -12,7 +12,7 @@ 'domain': 'cover', 'entity_category': None, 'entity_id': 'cover.bubendorff_blind', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -23,7 +23,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Bubendorff blind', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -62,7 +62,7 @@ 'domain': 'cover', 'entity_category': None, 'entity_id': 'cover.entrance_blinds', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -73,7 +73,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Entrance Blinds', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , diff --git a/tests/components/netatmo/snapshots/test_fan.ambr b/tests/components/netatmo/snapshots/test_fan.ambr index 958a8f79704..ba882d68e50 100644 --- a/tests/components/netatmo/snapshots/test_fan.ambr +++ b/tests/components/netatmo/snapshots/test_fan.ambr @@ -17,7 +17,7 @@ 'domain': 'fan', 'entity_category': None, 'entity_id': 'fan.centralized_ventilation_controler', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -28,7 +28,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Centralized ventilation controler', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index febc6f95bc6..8f4b357fc5f 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -111,7 +111,7 @@ }), 'manufacturer': 'Smarther', 'model': 'Smarther with Netatmo', - 'name': '', + 'name': 'Corridor', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Corridor', @@ -141,7 +141,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Energy Meter', - 'name': '', + 'name': 'Consumption meter', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -201,7 +201,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 1', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -231,7 +231,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 2', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -261,7 +261,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 3', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -291,7 +291,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 4', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -321,7 +321,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 5', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -351,7 +351,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Total', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -381,7 +381,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Gas', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -411,7 +411,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Hot water', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -441,7 +441,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Cold water', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -471,7 +471,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Écocompteur', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -771,7 +771,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Plug', - 'name': '', + 'name': 'Prise', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -951,7 +951,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'OpenTherm Modulating Thermostat', - 'name': '', + 'name': 'Bureau Modulate', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Bureau', @@ -981,7 +981,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'Smart Thermostat', - 'name': '', + 'name': 'Livingroom', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Livingroom', @@ -1011,7 +1011,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'Smart Valve', - 'name': '', + 'name': 'Valve1', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Entrada', @@ -1041,7 +1041,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'Smart Valve', - 'name': '', + 'name': 'Valve2', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Cocina', @@ -1049,6 +1049,36 @@ 'via_device_id': None, }) # --- +# name: test_devices[netatmo-91763b24c43d3e344f424e8b] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'https://my.netatmo.com/app/energy', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'netatmo', + '91763b24c43d3e344f424e8b', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Netatmo', + 'model': 'Climate', + 'name': 'MYHOME', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[netatmo-Home avg] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -1109,33 +1139,3 @@ 'via_device_id': None, }) # --- -# name: test_devices[netatmo-] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': 'https://my.netatmo.com/app/energy', - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'netatmo', - '', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Netatmo', - 'model': 'Smart Thermostat', - 'name': 'MYHOME', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }) -# --- diff --git a/tests/components/netatmo/snapshots/test_light.ambr b/tests/components/netatmo/snapshots/test_light.ambr index dabc7f8528f..fe5a8aac7d0 100644 --- a/tests/components/netatmo/snapshots/test_light.ambr +++ b/tests/components/netatmo/snapshots/test_light.ambr @@ -16,7 +16,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.bathroom_light', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -27,7 +27,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Bathroom light', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -127,7 +127,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.unknown_00_11_22_33_00_11_45_fe', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -138,7 +138,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Unknown 00:11:22:33:00:11:45:fe', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/netatmo/snapshots/test_select.ambr b/tests/components/netatmo/snapshots/test_select.ambr index 0a95049957e..ff68fc71c09 100644 --- a/tests/components/netatmo/snapshots/test_select.ambr +++ b/tests/components/netatmo/snapshots/test_select.ambr @@ -17,7 +17,7 @@ 'domain': 'select', 'entity_category': None, 'entity_id': 'select.myhome', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -28,7 +28,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'MYHOME', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index df92c644588..6ab1e4b1e1a 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_entity[sensor.baby_bedroom_co2-entry] +# name: test_entity[sensor.baby_bedroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -13,7 +13,67 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.baby_bedroom_co2', + 'entity_id': 'sensor.baby_bedroom_atmospheric_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:26:68:92-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.baby_bedroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Baby Bedroom Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1021.4', + }) +# --- +# name: test_entity[sensor.baby_bedroom_carbon_dioxide-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.baby_bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -25,47 +85,55 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:26:68:92-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.baby_bedroom_co2-state] +# name: test_entity[sensor.baby_bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Baby Bedroom CO2', + 'friendly_name': 'Baby Bedroom Carbon dioxide', 'latitude': 13.377726, 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.baby_bedroom_co2', + 'entity_id': 'sensor.baby_bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1053', }) # --- -# name: test_entity[sensor.baby_bedroom_health-entry] +# name: test_entity[sensor.baby_bedroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.baby_bedroom_health', + 'entity_id': 'sensor.baby_bedroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -75,32 +143,39 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:68:92-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.baby_bedroom_health-state] +# name: test_entity[sensor.baby_bedroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Baby Bedroom Health', - 'icon': 'mdi:cloud', + 'device_class': 'enum', + 'friendly_name': 'Baby Bedroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , - 'entity_id': 'sensor.baby_bedroom_health', + 'entity_id': 'sensor.baby_bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Fine', + 'state': 'fine', }) # --- # name: test_entity[sensor.baby_bedroom_humidity-entry] @@ -133,7 +208,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:26:68:92-humidity', 'unit_of_measurement': '%', }) @@ -187,7 +262,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:26:68:92-noise', 'unit_of_measurement': , }) @@ -211,63 +286,6 @@ 'state': '45', }) # --- -# name: test_entity[sensor.baby_bedroom_pressure-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.baby_bedroom_pressure', - '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': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:26:68:92-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.baby_bedroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Baby Bedroom Pressure', - 'latitude': 13.377726, - 'longitude': 52.516263, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.baby_bedroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1021.4', - }) -# --- # name: test_entity[sensor.baby_bedroom_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -277,7 +295,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.baby_bedroom_pressure_trend', @@ -291,18 +309,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:68:92-pressure_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.baby_bedroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Pressure trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- # name: test_entity[sensor.baby_bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -313,7 +344,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.baby_bedroom_reachability', @@ -327,18 +358,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:26:68:92-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.baby_bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.baby_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -363,6 +407,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -370,7 +417,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:26:68:92-temperature', 'unit_of_measurement': , }) @@ -403,7 +450,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.baby_bedroom_temperature_trend', @@ -417,20 +464,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:68:92-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.baby_bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Temperature trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- -# name: test_entity[sensor.baby_bedroom_wifi-entry] +# name: test_entity[sensor.baby_bedroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -439,10 +499,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.baby_bedroom_wifi', + 'entity_id': 'sensor.baby_bedroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -453,20 +513,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:68:92-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.baby_bedroom_wifi-state] - None +# name: test_entity[sensor.baby_bedroom_wi_fi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_wi_fi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- -# name: test_entity[sensor.bedroom_co2-entry] +# name: test_entity[sensor.bedroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -480,7 +553,65 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.bedroom_co2', + 'entity_id': 'sensor.bedroom_atmospheric_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:26:69:0c-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.bedroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Bedroom Atmospheric pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bedroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_entity[sensor.bedroom_carbon_dioxide-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.bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -492,45 +623,53 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:26:69:0c-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.bedroom_co2-state] +# name: test_entity[sensor.bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Bedroom CO2', + 'friendly_name': 'Bedroom Carbon dioxide', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.bedroom_co2', + 'entity_id': 'sensor.bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_entity[sensor.bedroom_health-entry] +# name: test_entity[sensor.bedroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.bedroom_health', + 'entity_id': 'sensor.bedroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -540,26 +679,33 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:69:0c-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.bedroom_health-state] +# name: test_entity[sensor.bedroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Bedroom Health', - 'icon': 'mdi:cloud', + 'device_class': 'enum', + 'friendly_name': 'Bedroom Health index', + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , - 'entity_id': 'sensor.bedroom_health', + 'entity_id': 'sensor.bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , @@ -596,7 +742,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:26:69:0c-humidity', 'unit_of_measurement': '%', }) @@ -648,7 +794,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:26:69:0c-noise', 'unit_of_measurement': , }) @@ -670,61 +816,6 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.bedroom_pressure-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.bedroom_pressure', - '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': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:26:69:0c-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.bedroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Bedroom Pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.bedroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_entity[sensor.bedroom_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -734,7 +825,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.bedroom_pressure_trend', @@ -748,18 +839,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:69:0c-pressure_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.bedroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Pressure trend', + }), + 'context': , + 'entity_id': 'sensor.bedroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -770,7 +872,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.bedroom_reachability', @@ -784,18 +886,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:26:69:0c-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) # --- # name: test_entity[sensor.bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -820,6 +935,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -827,7 +945,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:26:69:0c-temperature', 'unit_of_measurement': , }) @@ -858,7 +976,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.bedroom_temperature_trend', @@ -872,20 +990,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:69:0c-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Temperature trend', + }), + 'context': , + 'entity_id': 'sensor.bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- -# name: test_entity[sensor.bedroom_wifi-entry] +# name: test_entity[sensor.bedroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -894,10 +1023,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.bedroom_wifi', + 'entity_id': 'sensor.bedroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -908,20 +1037,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:69:0c-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.bedroom_wifi-state] - None +# name: test_entity[sensor.bedroom_wi_fi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.bedroom_wi_fi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- -# name: test_entity[sensor.bureau_modulate_battery_percent-entry] +# name: test_entity[sensor.bureau_modulate_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -935,8 +1077,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.bureau_modulate_battery_percent', - 'has_entity_name': False, + 'entity_id': 'sensor.bureau_modulate_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -947,7 +1089,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Bureau Modulate Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -956,23 +1098,70 @@ 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.bureau_modulate_battery_percent-state] +# name: test_entity[sensor.bureau_modulate_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Bureau Modulate Battery Percent', + 'friendly_name': 'Bureau Modulate Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.bureau_modulate_battery_percent', + 'entity_id': 'sensor.bureau_modulate_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '90', }) # --- +# name: test_entity[sensor.cold_water_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': , + 'entity_id': 'sensor.cold_water_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e#8-12:34:56:00:16:0e#8-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.cold_water_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Cold water None', + }), + 'context': , + 'entity_id': 'sensor.cold_water_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.cold_water_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -988,7 +1177,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.cold_water_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -999,7 +1188,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cold water Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1025,7 +1214,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.cold_water_reachability-entry] +# name: test_entity[sensor.consumption_meter_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1034,11 +1223,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.cold_water_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.consumption_meter_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1048,18 +1237,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Cold water Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#8-12:34:56:00:16:0e#8-reachable', + 'unique_id': '12:34:56:00:00:a1:4c:da-12:34:56:00:00:a1:4c:da-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.cold_water_reachability-state] - None +# name: test_entity[sensor.consumption_meter_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Consumption meter None', + }), + 'context': , + 'entity_id': 'sensor.consumption_meter_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.consumption_meter_power-entry] EntityRegistryEntrySnapshot({ @@ -1076,7 +1276,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.consumption_meter_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1087,7 +1287,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Consumption meter Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1113,42 +1313,6 @@ 'state': '476', }) # --- -# name: test_entity[sensor.consumption_meter_reachability-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.consumption_meter_reachability', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Consumption meter Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:00:a1:4c:da-12:34:56:00:00:a1:4c:da-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.consumption_meter_reachability-state] - None -# --- # name: test_entity[sensor.corridor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1164,7 +1328,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.corridor_humidity', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1175,7 +1339,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Corridor Humidity', + 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1201,6 +1365,53 @@ 'state': '67', }) # --- +# name: test_entity[sensor.ecocompteur_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': , + 'entity_id': 'sensor.ecocompteur_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e-12:34:56:00:16:0e-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.ecocompteur_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Écocompteur None', + }), + 'context': , + 'entity_id': 'sensor.ecocompteur_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.ecocompteur_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1216,7 +1427,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.ecocompteur_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1227,7 +1438,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Écocompteur Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1253,7 +1464,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.ecocompteur_reachability-entry] +# name: test_entity[sensor.gas_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1262,11 +1473,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.ecocompteur_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.gas_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1276,18 +1487,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Écocompteur Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e-12:34:56:00:16:0e-reachable', + 'unique_id': '12:34:56:00:16:0e#6-12:34:56:00:16:0e#6-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.ecocompteur_reachability-state] - None +# name: test_entity[sensor.gas_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Gas None', + }), + 'context': , + 'entity_id': 'sensor.gas_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.gas_power-entry] EntityRegistryEntrySnapshot({ @@ -1304,7 +1526,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.gas_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1315,7 +1537,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gas Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1341,43 +1563,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.gas_reachability-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.gas_reachability', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Gas Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#6-12:34:56:00:16:0e#6-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.gas_reachability-state] - None -# --- -# name: test_entity[sensor.home_avg_angle-entry] +# name: test_entity[sensor.home_avg_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1388,10 +1574,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_angle', + 'entity_id': 'sensor.home_avg_atmospheric_pressure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1400,20 +1586,42 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Angle', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-avg-windangle_value', - 'unit_of_measurement': '°', + 'unique_id': 'Home-avg-pressure', + 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_angle-state] - None +# name: test_entity[sensor.home_avg_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Home avg Atmospheric pressure', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_avg_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1010.4', + }) # --- # name: test_entity[sensor.home_avg_gust_angle-entry] EntityRegistryEntrySnapshot({ @@ -1426,7 +1634,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_avg_gust_angle', @@ -1440,18 +1648,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Angle', + 'original_icon': None, + 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_angle', 'unique_id': 'Home-avg-gustangle_value', 'unit_of_measurement': '°', }) # --- # name: test_entity[sensor.home_avg_gust_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Home avg Gust angle', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.home_avg_gust_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217.0', + }) # --- # name: test_entity[sensor.home_avg_gust_strength-entry] EntityRegistryEntrySnapshot({ @@ -1464,7 +1687,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_avg_gust_strength', @@ -1479,17 +1702,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gust Strength', + 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_strength', 'unique_id': 'Home-avg-guststrength', 'unit_of_measurement': , }) # --- # name: test_entity[sensor.home_avg_gust_strength-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'wind_speed', + 'friendly_name': 'Home avg Gust strength', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_avg_gust_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.0', + }) # --- # name: test_entity[sensor.home_avg_humidity-entry] EntityRegistryEntrySnapshot({ @@ -1545,7 +1784,7 @@ 'state': '63.2', }) # --- -# name: test_entity[sensor.home_avg_pressure-entry] +# name: test_entity[sensor.home_avg_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1559,7 +1798,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_pressure', + 'entity_id': 'sensor.home_avg_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1568,41 +1807,37 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pressure', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-avg-pressure', - 'unit_of_measurement': , + 'unique_id': 'Home-avg-windangle_value', + 'unit_of_measurement': '°', }) # --- -# name: test_entity[sensor.home_avg_pressure-state] +# name: test_entity[sensor.home_avg_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Home avg Pressure', + 'friendly_name': 'Home avg None', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': '°', }), 'context': , - 'entity_id': 'sensor.home_avg_pressure', + 'entity_id': 'sensor.home_avg_none', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1010.4', + 'state': '17.0', }) # --- -# name: test_entity[sensor.home_avg_rain-entry] +# name: test_entity[sensor.home_avg_precipitation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1616,7 +1851,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_rain', + 'entity_id': 'sensor.home_avg_precipitation', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1628,7 +1863,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain', + 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1637,26 +1872,26 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_rain-state] +# name: test_entity[sensor.home_avg_precipitation-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home avg Rain', + 'friendly_name': 'Home avg Precipitation', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_rain', + 'entity_id': 'sensor.home_avg_precipitation', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.1', }) # --- -# name: test_entity[sensor.home_avg_rain_last_hour-entry] +# name: test_entity[sensor.home_avg_precipitation_last_hour-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1667,10 +1902,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_rain_last_hour', + 'entity_id': 'sensor.home_avg_precipitation_last_hour', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1679,22 +1914,41 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain last hour', + 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_1', 'unique_id': 'Home-avg-sum_rain_1', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_rain_last_hour-state] - None +# name: test_entity[sensor.home_avg_precipitation_last_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Home avg Precipitation last hour', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_avg_precipitation_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) # --- -# name: test_entity[sensor.home_avg_rain_today-entry] +# name: test_entity[sensor.home_avg_precipitation_today-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1708,7 +1962,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_rain_today', + 'entity_id': 'sensor.home_avg_precipitation_today', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1720,28 +1974,28 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain today', + 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_24', 'unique_id': 'Home-avg-sum_rain_24', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_rain_today-state] +# name: test_entity[sensor.home_avg_precipitation_today-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home avg Rain today', + 'friendly_name': 'Home avg Precipitation today', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_rain_today', + 'entity_id': 'sensor.home_avg_precipitation_today', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1771,6 +2025,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1802,7 +2059,7 @@ 'state': '22.7', }) # --- -# name: test_entity[sensor.home_avg_wind_strength-entry] +# name: test_entity[sensor.home_avg_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1816,7 +2073,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_wind_strength', + 'entity_id': 'sensor.home_avg_wind_speed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1828,7 +2085,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wind Strength', + 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1837,26 +2094,26 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_wind_strength-state] +# name: test_entity[sensor.home_avg_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Home avg Wind Strength', + 'friendly_name': 'Home avg Wind speed', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_wind_strength', + 'entity_id': 'sensor.home_avg_wind_speed', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '15.0', }) # --- -# name: test_entity[sensor.home_max_angle-entry] +# name: test_entity[sensor.home_max_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1867,10 +2124,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_angle', + 'entity_id': 'sensor.home_max_atmospheric_pressure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1879,20 +2136,42 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Angle', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-max-windangle_value', - 'unit_of_measurement': '°', + 'unique_id': 'Home-max-pressure', + 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_angle-state] - None +# name: test_entity[sensor.home_max_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Home max Atmospheric pressure', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_max_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1014.4', + }) # --- # name: test_entity[sensor.home_max_gust_angle-entry] EntityRegistryEntrySnapshot({ @@ -1905,7 +2184,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_max_gust_angle', @@ -1919,18 +2198,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Angle', + 'original_icon': None, + 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_angle', 'unique_id': 'Home-max-gustangle_value', 'unit_of_measurement': '°', }) # --- # name: test_entity[sensor.home_max_gust_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Home max Gust angle', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.home_max_gust_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217', + }) # --- # name: test_entity[sensor.home_max_gust_strength-entry] EntityRegistryEntrySnapshot({ @@ -1943,7 +2237,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_max_gust_strength', @@ -1958,17 +2252,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gust Strength', + 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_strength', 'unique_id': 'Home-max-guststrength', 'unit_of_measurement': , }) # --- # name: test_entity[sensor.home_max_gust_strength-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'wind_speed', + 'friendly_name': 'Home max Gust strength', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_max_gust_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31', + }) # --- # name: test_entity[sensor.home_max_humidity-entry] EntityRegistryEntrySnapshot({ @@ -2024,7 +2334,7 @@ 'state': '76', }) # --- -# name: test_entity[sensor.home_max_pressure-entry] +# name: test_entity[sensor.home_max_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2038,7 +2348,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_pressure', + 'entity_id': 'sensor.home_max_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2047,41 +2357,37 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pressure', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-max-pressure', - 'unit_of_measurement': , + 'unique_id': 'Home-max-windangle_value', + 'unit_of_measurement': '°', }) # --- -# name: test_entity[sensor.home_max_pressure-state] +# name: test_entity[sensor.home_max_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Home max Pressure', + 'friendly_name': 'Home max None', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': '°', }), 'context': , - 'entity_id': 'sensor.home_max_pressure', + 'entity_id': 'sensor.home_max_none', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1014.4', + 'state': '17', }) # --- -# name: test_entity[sensor.home_max_rain-entry] +# name: test_entity[sensor.home_max_precipitation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2095,7 +2401,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_rain', + 'entity_id': 'sensor.home_max_precipitation', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2107,7 +2413,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain', + 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2116,26 +2422,26 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_rain-state] +# name: test_entity[sensor.home_max_precipitation-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home max Rain', + 'friendly_name': 'Home max Precipitation', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_rain', + 'entity_id': 'sensor.home_max_precipitation', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.5', }) # --- -# name: test_entity[sensor.home_max_rain_last_hour-entry] +# name: test_entity[sensor.home_max_precipitation_last_hour-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2146,10 +2452,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_rain_last_hour', + 'entity_id': 'sensor.home_max_precipitation_last_hour', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2158,22 +2464,41 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain last hour', + 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_1', 'unique_id': 'Home-max-sum_rain_1', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_rain_last_hour-state] - None +# name: test_entity[sensor.home_max_precipitation_last_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Home max Precipitation last hour', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_max_precipitation_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.2', + }) # --- -# name: test_entity[sensor.home_max_rain_today-entry] +# name: test_entity[sensor.home_max_precipitation_today-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2187,7 +2512,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_rain_today', + 'entity_id': 'sensor.home_max_precipitation_today', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2199,28 +2524,28 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain today', + 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_24', 'unique_id': 'Home-max-sum_rain_24', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_rain_today-state] +# name: test_entity[sensor.home_max_precipitation_today-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home max Rain today', + 'friendly_name': 'Home max Precipitation today', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_rain_today', + 'entity_id': 'sensor.home_max_precipitation_today', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2250,6 +2575,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2281,7 +2609,7 @@ 'state': '27.4', }) # --- -# name: test_entity[sensor.home_max_wind_strength-entry] +# name: test_entity[sensor.home_max_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2295,7 +2623,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_wind_strength', + 'entity_id': 'sensor.home_max_wind_speed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2307,7 +2635,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wind Strength', + 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2316,25 +2644,72 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_wind_strength-state] +# name: test_entity[sensor.home_max_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Home max Wind Strength', + 'friendly_name': 'Home max Wind speed', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_wind_strength', + 'entity_id': 'sensor.home_max_wind_speed', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '15', }) # --- +# name: test_entity[sensor.hot_water_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': , + 'entity_id': 'sensor.hot_water_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e#7-12:34:56:00:16:0e#7-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.hot_water_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Hot water None', + }), + 'context': , + 'entity_id': 'sensor.hot_water_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.hot_water_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2350,7 +2725,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.hot_water_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2361,7 +2736,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Hot water Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2387,43 +2762,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.hot_water_reachability-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.hot_water_reachability', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Hot water Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#7-12:34:56:00:16:0e#7-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.hot_water_reachability-state] - None -# --- -# name: test_entity[sensor.kitchen_co2-entry] +# name: test_entity[sensor.kitchen_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2437,7 +2776,67 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.kitchen_co2', + 'entity_id': 'sensor.kitchen_atmospheric_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:25:cf:a8-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.kitchen_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Kitchen Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.kitchen_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity[sensor.kitchen_carbon_dioxide-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.kitchen_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2449,45 +2848,55 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:25:cf:a8-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.kitchen_co2-state] +# name: test_entity[sensor.kitchen_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Kitchen CO2', + 'friendly_name': 'Kitchen Carbon dioxide', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.kitchen_co2', + 'entity_id': 'sensor.kitchen_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.kitchen_health-entry] +# name: test_entity[sensor.kitchen_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.kitchen_health', + 'entity_id': 'sensor.kitchen_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2497,30 +2906,39 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:25:cf:a8-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.kitchen_health-state] +# name: test_entity[sensor.kitchen_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Kitchen Health', - 'icon': 'mdi:cloud', + 'device_class': 'enum', + 'friendly_name': 'Kitchen Health index', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , - 'entity_id': 'sensor.kitchen_health', + 'entity_id': 'sensor.kitchen_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_humidity-entry] @@ -2553,7 +2971,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:25:cf:a8-humidity', 'unit_of_measurement': '%', }) @@ -2564,6 +2982,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'humidity', 'friendly_name': 'Kitchen Humidity', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': '%', }), @@ -2572,7 +2992,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_noise-entry] @@ -2605,7 +3025,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:25:cf:a8-noise', 'unit_of_measurement': , }) @@ -2616,6 +3036,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'sound_pressure', 'friendly_name': 'Kitchen Noise', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -2624,62 +3046,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.kitchen_pressure-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.kitchen_pressure', - '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': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:25:cf:a8-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.kitchen_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Kitchen Pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.kitchen_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_pressure_trend-entry] @@ -2691,7 +3058,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.kitchen_pressure_trend', @@ -2705,18 +3072,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:25:cf:a8-pressure_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.kitchen_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Pressure trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.kitchen_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- # name: test_entity[sensor.kitchen_reachability-entry] EntityRegistryEntrySnapshot({ @@ -2727,7 +3107,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.kitchen_reachability', @@ -2741,18 +3121,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:25:cf:a8-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.kitchen_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.kitchen_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.kitchen_temperature-entry] EntityRegistryEntrySnapshot({ @@ -2777,6 +3170,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2784,7 +3180,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:25:cf:a8-temperature', 'unit_of_measurement': , }) @@ -2795,6 +3191,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'temperature', 'friendly_name': 'Kitchen Temperature', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -2803,7 +3201,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_temperature_trend-entry] @@ -2815,7 +3213,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.kitchen_temperature_trend', @@ -2829,20 +3227,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:25:cf:a8-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.kitchen_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Temperature trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.kitchen_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- -# name: test_entity[sensor.kitchen_wifi-entry] +# name: test_entity[sensor.kitchen_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2851,10 +3262,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.kitchen_wifi', + 'entity_id': 'sensor.kitchen_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2865,18 +3276,78 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:25:cf:a8-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.kitchen_wifi-state] - None +# name: test_entity[sensor.kitchen_wi_fi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.kitchen_wi_fi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Full', + }) +# --- +# name: test_entity[sensor.line_1_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': , + 'entity_id': 'sensor.line_1_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e#0-12:34:56:00:16:0e#0-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.line_1_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 1 None', + }), + 'context': , + 'entity_id': 'sensor.line_1_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_1_power-entry] EntityRegistryEntrySnapshot({ @@ -2893,7 +3364,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_1_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2904,7 +3375,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 1 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2930,7 +3401,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_1_reachability-entry] +# name: test_entity[sensor.line_2_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2939,11 +3410,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_1_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.line_2_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2953,18 +3424,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Line 1 Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#0-12:34:56:00:16:0e#0-reachable', + 'unique_id': '12:34:56:00:16:0e#1-12:34:56:00:16:0e#1-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_1_reachability-state] - None +# name: test_entity[sensor.line_2_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 2 None', + }), + 'context': , + 'entity_id': 'sensor.line_2_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_2_power-entry] EntityRegistryEntrySnapshot({ @@ -2981,7 +3463,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_2_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2992,7 +3474,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 2 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3018,7 +3500,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_2_reachability-entry] +# name: test_entity[sensor.line_3_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3027,11 +3509,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_2_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.line_3_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3041,18 +3523,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Line 2 Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#1-12:34:56:00:16:0e#1-reachable', + 'unique_id': '12:34:56:00:16:0e#2-12:34:56:00:16:0e#2-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_2_reachability-state] - None +# name: test_entity[sensor.line_3_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 3 None', + }), + 'context': , + 'entity_id': 'sensor.line_3_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_3_power-entry] EntityRegistryEntrySnapshot({ @@ -3069,7 +3562,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_3_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3080,7 +3573,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 3 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3106,7 +3599,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_3_reachability-entry] +# name: test_entity[sensor.line_4_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3115,11 +3608,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_3_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.line_4_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3129,18 +3622,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Line 3 Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#2-12:34:56:00:16:0e#2-reachable', + 'unique_id': '12:34:56:00:16:0e#3-12:34:56:00:16:0e#3-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_3_reachability-state] - None +# name: test_entity[sensor.line_4_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 4 None', + }), + 'context': , + 'entity_id': 'sensor.line_4_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_4_power-entry] EntityRegistryEntrySnapshot({ @@ -3157,7 +3661,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_4_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3168,7 +3672,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 4 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3194,7 +3698,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_4_reachability-entry] +# name: test_entity[sensor.line_5_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3203,11 +3707,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_4_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.line_5_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3217,18 +3721,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Line 4 Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#3-12:34:56:00:16:0e#3-reachable', + 'unique_id': '12:34:56:00:16:0e#4-12:34:56:00:16:0e#4-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_4_reachability-state] - None +# name: test_entity[sensor.line_5_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 5 None', + }), + 'context': , + 'entity_id': 'sensor.line_5_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_5_power-entry] EntityRegistryEntrySnapshot({ @@ -3245,7 +3760,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_5_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3256,7 +3771,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 5 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3282,95 +3797,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_5_reachability-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.line_5_reachability', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Line 5 Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#4-12:34:56:00:16:0e#4-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.line_5_reachability-state] - None -# --- -# name: test_entity[sensor.livingroom_battery_percent-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.livingroom_battery_percent', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Livingroom Battery Percent', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '2746182631-12:34:56:00:01:ae-battery', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entity[sensor.livingroom_battery_percent-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'battery', - 'friendly_name': 'Livingroom Battery Percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.livingroom_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '75', - }) -# --- -# name: test_entity[sensor.livingroom_co2-entry] +# name: test_entity[sensor.livingroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3384,7 +3811,119 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.livingroom_co2', + 'entity_id': 'sensor.livingroom_atmospheric_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:26:65:14-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.livingroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Livingroom Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.livingroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity[sensor.livingroom_battery-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.livingroom_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '2746182631-12:34:56:00:01:ae-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity[sensor.livingroom_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'battery', + 'friendly_name': 'Livingroom Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.livingroom_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_entity[sensor.livingroom_carbon_dioxide-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.livingroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3396,45 +3935,55 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:26:65:14-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.livingroom_co2-state] +# name: test_entity[sensor.livingroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Livingroom CO2', + 'friendly_name': 'Livingroom Carbon dioxide', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.livingroom_co2', + 'entity_id': 'sensor.livingroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.livingroom_health-entry] +# name: test_entity[sensor.livingroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.livingroom_health', + 'entity_id': 'sensor.livingroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3444,30 +3993,39 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:65:14-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.livingroom_health-state] +# name: test_entity[sensor.livingroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Livingroom Health', - 'icon': 'mdi:cloud', + 'device_class': 'enum', + 'friendly_name': 'Livingroom Health index', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , - 'entity_id': 'sensor.livingroom_health', + 'entity_id': 'sensor.livingroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_humidity-entry] @@ -3500,7 +4058,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:26:65:14-humidity', 'unit_of_measurement': '%', }) @@ -3511,6 +4069,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'humidity', 'friendly_name': 'Livingroom Humidity', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': '%', }), @@ -3519,7 +4079,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_noise-entry] @@ -3552,7 +4112,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:26:65:14-noise', 'unit_of_measurement': , }) @@ -3563,6 +4123,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'sound_pressure', 'friendly_name': 'Livingroom Noise', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -3571,62 +4133,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.livingroom_pressure-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.livingroom_pressure', - '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': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:26:65:14-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.livingroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Livingroom Pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.livingroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_pressure_trend-entry] @@ -3638,7 +4145,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.livingroom_pressure_trend', @@ -3652,18 +4159,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:65:14-pressure_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.livingroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Pressure trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.livingroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- # name: test_entity[sensor.livingroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -3674,7 +4194,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.livingroom_reachability', @@ -3688,18 +4208,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:26:65:14-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.livingroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.livingroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.livingroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -3724,6 +4257,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3731,7 +4267,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:26:65:14-temperature', 'unit_of_measurement': , }) @@ -3742,6 +4278,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'temperature', 'friendly_name': 'Livingroom Temperature', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -3750,7 +4288,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_temperature_trend-entry] @@ -3762,7 +4300,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.livingroom_temperature_trend', @@ -3776,20 +4314,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:65:14-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.livingroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Temperature trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.livingroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- -# name: test_entity[sensor.livingroom_wifi-entry] +# name: test_entity[sensor.livingroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3798,10 +4349,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.livingroom_wifi', + 'entity_id': 'sensor.livingroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3812,20 +4363,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:65:14-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.livingroom_wifi-state] - None +# name: test_entity[sensor.livingroom_wi_fi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.livingroom_wi_fi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- -# name: test_entity[sensor.parents_bedroom_co2-entry] +# name: test_entity[sensor.parents_bedroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3839,7 +4403,67 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.parents_bedroom_co2', + 'entity_id': 'sensor.parents_bedroom_atmospheric_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:3e:c5:46-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.parents_bedroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Parents Bedroom Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1014.5', + }) +# --- +# name: test_entity[sensor.parents_bedroom_carbon_dioxide-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.parents_bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3851,47 +4475,55 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:3e:c5:46-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.parents_bedroom_co2-state] +# name: test_entity[sensor.parents_bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Parents Bedroom CO2', + 'friendly_name': 'Parents Bedroom Carbon dioxide', 'latitude': 13.377726, 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.parents_bedroom_co2', + 'entity_id': 'sensor.parents_bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '494', }) # --- -# name: test_entity[sensor.parents_bedroom_health-entry] +# name: test_entity[sensor.parents_bedroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.parents_bedroom_health', + 'entity_id': 'sensor.parents_bedroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3901,32 +4533,39 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:3e:c5:46-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.parents_bedroom_health-state] +# name: test_entity[sensor.parents_bedroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Parents Bedroom Health', - 'icon': 'mdi:cloud', + 'device_class': 'enum', + 'friendly_name': 'Parents Bedroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , - 'entity_id': 'sensor.parents_bedroom_health', + 'entity_id': 'sensor.parents_bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Fine', + 'state': 'fine', }) # --- # name: test_entity[sensor.parents_bedroom_humidity-entry] @@ -3959,7 +4598,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:3e:c5:46-humidity', 'unit_of_measurement': '%', }) @@ -4013,7 +4652,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:3e:c5:46-noise', 'unit_of_measurement': , }) @@ -4037,63 +4676,6 @@ 'state': '42', }) # --- -# name: test_entity[sensor.parents_bedroom_pressure-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.parents_bedroom_pressure', - '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': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:3e:c5:46-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.parents_bedroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Parents Bedroom Pressure', - 'latitude': 13.377726, - 'longitude': 52.516263, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.parents_bedroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1014.5', - }) -# --- # name: test_entity[sensor.parents_bedroom_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4103,7 +4685,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.parents_bedroom_pressure_trend', @@ -4117,18 +4699,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:3e:c5:46-pressure_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.parents_bedroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Pressure trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- # name: test_entity[sensor.parents_bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -4139,7 +4734,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.parents_bedroom_reachability', @@ -4153,18 +4748,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:3e:c5:46-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.parents_bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.parents_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -4189,6 +4797,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4196,7 +4807,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:3e:c5:46-temperature', 'unit_of_measurement': , }) @@ -4229,7 +4840,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.parents_bedroom_temperature_trend', @@ -4243,20 +4854,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:3e:c5:46-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.parents_bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Temperature trend', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- -# name: test_entity[sensor.parents_bedroom_wifi-entry] +# name: test_entity[sensor.parents_bedroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4265,10 +4889,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.parents_bedroom_wifi', + 'entity_id': 'sensor.parents_bedroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4279,18 +4903,78 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:3e:c5:46-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.parents_bedroom_wifi-state] - None +# name: test_entity[sensor.parents_bedroom_wi_fi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_wi_fi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) +# --- +# name: test_entity[sensor.prise_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': , + 'entity_id': 'sensor.prise_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:00:12:ac:f2-12:34:56:80:00:12:ac:f2-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.prise_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Prise None', + }), + 'context': , + 'entity_id': 'sensor.prise_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.prise_power-entry] EntityRegistryEntrySnapshot({ @@ -4307,7 +4991,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.prise_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4318,7 +5002,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Prise Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4344,7 +5028,7 @@ 'state': '0', }) # --- -# name: test_entity[sensor.prise_reachability-entry] +# name: test_entity[sensor.total_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4353,11 +5037,11 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.prise_reachability', - 'has_entity_name': False, + 'entity_id': 'sensor.total_none', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4367,18 +5051,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Prise Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:80:00:12:ac:f2-12:34:56:80:00:12:ac:f2-reachable', + 'unique_id': '12:34:56:00:16:0e#5-12:34:56:00:16:0e#5-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.prise_reachability-state] - None +# name: test_entity[sensor.total_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Total None', + }), + 'context': , + 'entity_id': 'sensor.total_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.total_power-entry] EntityRegistryEntrySnapshot({ @@ -4395,7 +5090,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.total_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4406,7 +5101,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4432,43 +5127,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.total_reachability-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.total_reachability', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Total Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#5-12:34:56:00:16:0e#5-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.total_reachability-state] - None -# --- -# name: test_entity[sensor.valve1_battery_percent-entry] +# name: test_entity[sensor.valve1_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4482,111 +5141,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.valve1_battery_percent', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Valve1 Battery Percent', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '2833524037-12:34:56:03:a5:54-battery', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entity[sensor.valve1_battery_percent-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'battery', - 'friendly_name': 'Valve1 Battery Percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.valve1_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '90', - }) -# --- -# name: test_entity[sensor.valve2_battery_percent-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.valve2_battery_percent', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Valve2 Battery Percent', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '2940411577-12:34:56:03:a0:ac-battery', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entity[sensor.valve2_battery_percent-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'battery', - 'friendly_name': 'Valve2 Battery Percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.valve2_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '90', - }) -# --- -# name: test_entity[sensor.villa_bathroom_battery_percent-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.villa_bathroom_battery_percent', + 'entity_id': 'sensor.valve1_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4598,33 +5153,85 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:80:7e:18-battery_percent', + 'unique_id': '2833524037-12:34:56:03:a5:54-battery', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_bathroom_battery_percent-state] +# name: test_entity[sensor.valve1_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Bathroom Battery Percent', + 'friendly_name': 'Valve1 Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_bathroom_battery_percent', + 'entity_id': 'sensor.valve1_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '55', + 'state': '90', }) # --- -# name: test_entity[sensor.villa_bathroom_co2-entry] +# name: test_entity[sensor.valve2_battery-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.valve2_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '2940411577-12:34:56:03:a0:ac-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity[sensor.valve2_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'battery', + 'friendly_name': 'Valve2 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.valve2_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_entity[sensor.villa_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4638,7 +5245,119 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_bathroom_co2', + 'entity_id': 'sensor.villa_atmospheric_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:80:bb:26-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Villa Atmospheric pressure', + 'latitude': 46.123456, + 'longitude': 6.1234567, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1026.8', + }) +# --- +# name: test_entity[sensor.villa_bathroom_battery-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.villa_bathroom_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': '12:34:56:80:7e:18-battery_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity[sensor.villa_bathroom_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'battery', + 'friendly_name': 'Villa Bathroom Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55', + }) +# --- +# name: test_entity[sensor.villa_bathroom_carbon_dioxide-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.villa_bathroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4650,26 +5369,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:80:7e:18-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.villa_bathroom_co2-state] +# name: test_entity[sensor.villa_bathroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Villa Bathroom CO2', + 'friendly_name': 'Villa Bathroom Carbon dioxide', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.villa_bathroom_co2', + 'entity_id': 'sensor.villa_bathroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4706,7 +5425,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:7e:18-humidity', 'unit_of_measurement': '%', }) @@ -4737,7 +5456,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bathroom_radio', @@ -4751,18 +5470,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:7e:18-rf_status', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_bathroom_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bathroom Radio', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Full', + }) # --- # name: test_entity[sensor.villa_bathroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -4773,7 +5503,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bathroom_reachability', @@ -4787,18 +5517,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:7e:18-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_bathroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bathroom Reachability', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_bathroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -4823,6 +5564,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4830,7 +5574,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:7e:18-temperature', 'unit_of_measurement': , }) @@ -4861,7 +5605,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_bathroom_temperature_trend', @@ -4875,20 +5619,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:7e:18-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_bathroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bathroom Temperature trend', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stable', + }) # --- -# name: test_entity[sensor.villa_bedroom_battery_percent-entry] +# name: test_entity[sensor.villa_bedroom_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4902,7 +5657,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_bedroom_battery_percent', + 'entity_id': 'sensor.villa_bedroom_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4914,33 +5669,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:80:44:92-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_bedroom_battery_percent-state] +# name: test_entity[sensor.villa_bedroom_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Bedroom Battery Percent', + 'friendly_name': 'Villa Bedroom Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_bedroom_battery_percent', + 'entity_id': 'sensor.villa_bedroom_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '28', }) # --- -# name: test_entity[sensor.villa_bedroom_co2-entry] +# name: test_entity[sensor.villa_bedroom_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4954,7 +5709,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_bedroom_co2', + 'entity_id': 'sensor.villa_bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4966,26 +5721,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:80:44:92-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.villa_bedroom_co2-state] +# name: test_entity[sensor.villa_bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Villa Bedroom CO2', + 'friendly_name': 'Villa Bedroom Carbon dioxide', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.villa_bedroom_co2', + 'entity_id': 'sensor.villa_bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , @@ -5022,7 +5777,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:44:92-humidity', 'unit_of_measurement': '%', }) @@ -5053,7 +5808,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bedroom_radio', @@ -5067,18 +5822,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:44:92-rf_status', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_bedroom_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bedroom Radio', + }), + 'context': , + 'entity_id': 'sensor.villa_bedroom_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- # name: test_entity[sensor.villa_bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -5089,7 +5855,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bedroom_reachability', @@ -5103,18 +5869,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:44:92-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bedroom Reachability', + }), + 'context': , + 'entity_id': 'sensor.villa_bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -5139,6 +5916,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5146,7 +5926,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:44:92-temperature', 'unit_of_measurement': , }) @@ -5177,7 +5957,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_bedroom_temperature_trend', @@ -5191,20 +5971,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:44:92-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bedroom Temperature trend', + }), + 'context': , + 'entity_id': 'sensor.villa_bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stable', + }) # --- -# name: test_entity[sensor.villa_co2-entry] +# name: test_entity[sensor.villa_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5218,7 +6009,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_co2', + 'entity_id': 'sensor.villa_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5230,73 +6021,35 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:80:bb:26-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.villa_co2-state] +# name: test_entity[sensor.villa_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Villa CO2', + 'friendly_name': 'Villa Carbon dioxide', 'latitude': 46.123456, 'longitude': 6.1234567, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.villa_co2', + 'entity_id': 'sensor.villa_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1339', }) # --- -# name: test_entity[sensor.villa_garden_angle-entry] - 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.villa_garden_angle', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Angle', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:03:1b:e4-windangle_value', - 'unit_of_measurement': '°', - }) -# --- -# name: test_entity[sensor.villa_garden_angle-state] - None -# --- -# name: test_entity[sensor.villa_garden_battery_percent-entry] +# name: test_entity[sensor.villa_garden_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5310,7 +6063,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_garden_battery_percent', + 'entity_id': 'sensor.villa_garden_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5322,80 +6075,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:03:1b:e4-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_garden_battery_percent-state] +# name: test_entity[sensor.villa_garden_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Garden Battery Percent', + 'friendly_name': 'Villa Garden Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_garden_battery_percent', + 'entity_id': 'sensor.villa_garden_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '85', }) # --- -# name: test_entity[sensor.villa_garden_direction-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.villa_garden_direction', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Direction', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:03:1b:e4-windangle', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.villa_garden_direction-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Villa Garden Direction', - 'icon': 'mdi:compass-outline', - }), - 'context': , - 'entity_id': 'sensor.villa_garden_direction', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'SW', - }) -# --- # name: test_entity[sensor.villa_garden_gust_angle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5407,7 +6112,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_gust_angle', @@ -5421,29 +6126,53 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Angle', + 'original_icon': None, + 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_angle', 'unique_id': '12:34:56:03:1b:e4-gustangle_value', 'unit_of_measurement': '°', }) # --- # name: test_entity[sensor.villa_garden_gust_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Gust angle', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_gust_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '206', + }) # --- # name: test_entity[sensor.villa_garden_gust_direction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_gust_direction', @@ -5456,19 +6185,41 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Direction', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gust direction', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_direction', 'unique_id': '12:34:56:03:1b:e4-gustangle', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_garden_gust_direction-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', + 'friendly_name': 'Villa Garden Gust direction', + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), + }), + 'context': , + 'entity_id': 'sensor.villa_garden_gust_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 's', + }) # --- # name: test_entity[sensor.villa_garden_gust_strength-entry] EntityRegistryEntrySnapshot({ @@ -5481,7 +6232,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_gust_strength', @@ -5496,17 +6247,31 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gust Strength', + 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_strength', 'unique_id': '12:34:56:03:1b:e4-guststrength', 'unit_of_measurement': , }) # --- # name: test_entity[sensor.villa_garden_gust_strength-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'wind_speed', + 'friendly_name': 'Villa Garden Gust strength', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_garden_gust_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9', + }) # --- # name: test_entity[sensor.villa_garden_radio-entry] EntityRegistryEntrySnapshot({ @@ -5517,7 +6282,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_garden_radio', @@ -5531,18 +6296,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:03:1b:e4-rf_status', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_garden_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Radio', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Full', + }) # --- # name: test_entity[sensor.villa_garden_reachability-entry] EntityRegistryEntrySnapshot({ @@ -5553,7 +6329,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_garden_reachability', @@ -5567,20 +6343,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:03:1b:e4-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_garden_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Reachability', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- -# name: test_entity[sensor.villa_garden_wind_strength-entry] +# name: test_entity[sensor.villa_garden_wind_angle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5594,7 +6381,127 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_garden_wind_strength', + 'entity_id': 'sensor.villa_garden_wind_angle', + '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': 'Wind angle', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_angle', + 'unique_id': '12:34:56:03:1b:e4-windangle_value', + 'unit_of_measurement': '°', + }) +# --- +# name: test_entity[sensor.villa_garden_wind_angle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Wind angle', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_wind_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217', + }) +# --- +# name: test_entity[sensor.villa_garden_wind_direction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.villa_garden_wind_direction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind direction', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': '12:34:56:03:1b:e4-windangle', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.villa_garden_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', + 'friendly_name': 'Villa Garden Wind direction', + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), + }), + 'context': , + 'entity_id': 'sensor.villa_garden_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'sw', + }) +# --- +# name: test_entity[sensor.villa_garden_wind_speed-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.villa_garden_wind_speed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5606,26 +6513,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wind Strength', + 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wind_strength', 'unique_id': '12:34:56:03:1b:e4-windstrength', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.villa_garden_wind_strength-state] +# name: test_entity[sensor.villa_garden_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Villa Garden Wind Strength', + 'friendly_name': 'Villa Garden Wind speed', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.villa_garden_wind_strength', + 'entity_id': 'sensor.villa_garden_wind_speed', 'last_changed': , 'last_reported': , 'last_updated': , @@ -5662,7 +6569,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:bb:26-humidity', 'unit_of_measurement': '%', }) @@ -5716,7 +6623,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:80:bb:26-noise', 'unit_of_measurement': , }) @@ -5740,7 +6647,7 @@ 'state': '35', }) # --- -# name: test_entity[sensor.villa_outdoor_battery_percent-entry] +# name: test_entity[sensor.villa_outdoor_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5754,7 +6661,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_outdoor_battery_percent', + 'entity_id': 'sensor.villa_outdoor_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5766,30 +6673,30 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:80:1c:42-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_outdoor_battery_percent-state] +# name: test_entity[sensor.villa_outdoor_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Outdoor Battery Percent', + 'friendly_name': 'Villa Outdoor Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_outdoor_battery_percent', + 'entity_id': 'sensor.villa_outdoor_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '27', }) # --- # name: test_entity[sensor.villa_outdoor_humidity-entry] @@ -5822,7 +6729,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:1c:42-humidity', 'unit_of_measurement': '%', }) @@ -5853,7 +6760,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_outdoor_radio', @@ -5867,18 +6774,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:1c:42-rf_status', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_outdoor_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Outdoor Radio', + }), + 'context': , + 'entity_id': 'sensor.villa_outdoor_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- # name: test_entity[sensor.villa_outdoor_reachability-entry] EntityRegistryEntrySnapshot({ @@ -5889,7 +6807,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_outdoor_reachability', @@ -5903,18 +6821,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:1c:42-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_outdoor_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Outdoor Reachability', + }), + 'context': , + 'entity_id': 'sensor.villa_outdoor_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) # --- # name: test_entity[sensor.villa_outdoor_temperature-entry] EntityRegistryEntrySnapshot({ @@ -5939,6 +6868,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5946,7 +6878,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:1c:42-temperature', 'unit_of_measurement': , }) @@ -5977,7 +6909,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_outdoor_temperature_trend', @@ -5991,74 +6923,28 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:1c:42-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_outdoor_temperature_trend-state] - None -# --- -# name: test_entity[sensor.villa_pressure-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.villa_pressure', - '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': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:bb:26-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_pressure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Villa Pressure', - 'latitude': 46.123456, - 'longitude': 6.1234567, - 'state_class': , - 'unit_of_measurement': , + 'friendly_name': 'Villa Outdoor Temperature trend', }), 'context': , - 'entity_id': 'sensor.villa_pressure', + 'entity_id': 'sensor.villa_outdoor_temperature_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1026.8', + 'state': 'unavailable', }) # --- # name: test_entity[sensor.villa_pressure_trend-entry] @@ -6070,7 +6956,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_pressure_trend', @@ -6084,20 +6970,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:80:bb:26-pressure_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Pressure trend', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'up', + }) # --- -# name: test_entity[sensor.villa_rain_battery_percent-entry] +# name: test_entity[sensor.villa_rain_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -6111,7 +7010,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_rain_battery_percent', + 'entity_id': 'sensor.villa_rain_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -6123,32 +7022,191 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:80:c1:ea-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_rain_battery_percent-state] +# name: test_entity[sensor.villa_rain_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Rain Battery Percent', + 'friendly_name': 'Villa Rain Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_rain_battery_percent', + 'entity_id': 'sensor.villa_rain_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '21', }) # --- +# name: test_entity[sensor.villa_rain_precipitation-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.villa_rain_precipitation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'rain', + 'unique_id': '12:34:56:80:c1:ea-rain', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Precipitation', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_precipitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.7', + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_last_hour-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.villa_rain_precipitation_last_hour', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation last hour', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sum_rain_1', + 'unique_id': '12:34:56:80:c1:ea-sum_rain_1', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_last_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Precipitation last hour', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_precipitation_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_today-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.villa_rain_precipitation_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation today', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sum_rain_24', + 'unique_id': '12:34:56:80:c1:ea-sum_rain_24', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Precipitation today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.9', + }) +# --- # name: test_entity[sensor.villa_rain_radio-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6158,7 +7216,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_rain_radio', @@ -6172,159 +7230,28 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:c1:ea-rf_status', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_rain_radio-state] - None -# --- -# name: test_entity[sensor.villa_rain_rain-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.villa_rain_rain', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rain', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:c1:ea-rain', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_rain_rain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'device_class': 'precipitation', - 'friendly_name': 'Villa Rain Rain', - 'state_class': , - 'unit_of_measurement': , + 'friendly_name': 'Villa Rain Radio', }), 'context': , - 'entity_id': 'sensor.villa_rain_rain', + 'entity_id': 'sensor.villa_rain_radio', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3.7', - }) -# --- -# name: test_entity[sensor.villa_rain_rain_last_hour-entry] - 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.villa_rain_rain_last_hour', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rain last hour', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:c1:ea-sum_rain_1', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_rain_rain_last_hour-state] - None -# --- -# name: test_entity[sensor.villa_rain_rain_today-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.villa_rain_rain_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rain today', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:c1:ea-sum_rain_24', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_rain_rain_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'precipitation', - 'friendly_name': 'Villa Rain Rain today', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.villa_rain_rain_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6.9', + 'state': 'Medium', }) # --- # name: test_entity[sensor.villa_rain_reachability-entry] @@ -6336,7 +7263,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_rain_reachability', @@ -6350,18 +7277,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:c1:ea-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_rain_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Rain Reachability', + }), + 'context': , + 'entity_id': 'sensor.villa_rain_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_reachability-entry] EntityRegistryEntrySnapshot({ @@ -6372,7 +7310,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_reachability', @@ -6386,18 +7324,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:bb:26-reachable', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Reachability', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_temperature-entry] EntityRegistryEntrySnapshot({ @@ -6422,6 +7373,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -6429,7 +7383,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:bb:26-temperature', 'unit_of_measurement': , }) @@ -6462,7 +7416,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_temperature_trend', @@ -6476,20 +7430,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:bb:26-temp_trend', 'unit_of_measurement': None, }) # --- # name: test_entity[sensor.villa_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Temperature trend', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stable', + }) # --- -# name: test_entity[sensor.villa_wifi-entry] +# name: test_entity[sensor.villa_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -6498,10 +7465,10 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_wifi', + 'entity_id': 'sensor.villa_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -6512,16 +7479,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:80:bb:26-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.villa_wifi-state] - None +# name: test_entity[sensor.villa_wi_fi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Wi-Fi', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_wi_fi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- diff --git a/tests/components/netatmo/snapshots/test_switch.ambr b/tests/components/netatmo/snapshots/test_switch.ambr index 22c41aefd42..4244917d86f 100644 --- a/tests/components/netatmo/snapshots/test_switch.ambr +++ b/tests/components/netatmo/snapshots/test_switch.ambr @@ -12,7 +12,7 @@ 'domain': 'switch', 'entity_category': None, 'entity_id': 'switch.prise', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -23,7 +23,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Prise', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/netatmo/test_binary_sensor.py b/tests/components/netatmo/test_binary_sensor.py new file mode 100644 index 00000000000..53aea461fde --- /dev/null +++ b/tests/components/netatmo/test_binary_sensor.py @@ -0,0 +1,31 @@ +"""Support for Netatmo binary sensors.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry +from tests.components.netatmo.common import snapshot_platform_entities + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity( + hass: HomeAssistant, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test entities.""" + await snapshot_platform_entities( + hass, + config_entry, + Platform.BINARY_SENSOR, + entity_registry, + snapshot, + ) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 7866e448734..933f782c9d9 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyatmo.const import ALL_SCOPES -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( @@ -15,7 +15,9 @@ from homeassistant.components.netatmo.const import ( OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .conftest import CLIENT_ID @@ -37,7 +39,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await hass.config_entries.flow.async_init( @@ -53,7 +55,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -140,28 +142,28 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -198,28 +200,28 @@ async def test_option_flow_wrong_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -274,7 +276,7 @@ async def test_reauth( new_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert new_entry.state == config_entries.ConfigEntryState.LOADED + assert new_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -282,7 +284,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -319,8 +321,8 @@ async def test_reauth( new_entry2 = hass.config_entries.async_entries(DOMAIN)[0] - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" - assert new_entry2.state == config_entries.ConfigEntryState.LOADED + assert new_entry2.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index f7c31d7681c..566bc72426b 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN from homeassistant.components.netatmo.const import ( diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index e4869b73e2e..55af74b3373 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -9,12 +9,12 @@ from pyatmo.const import ALL_SCOPES import pytest from syrupy import SnapshotAssertion -from homeassistant import config_entries from homeassistant.components import cloud from homeassistant.components.netatmo import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.core import CoreState, HomeAssistant -import homeassistant.helpers.device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -31,6 +31,7 @@ from tests.common import ( async_get_persistent_notifications, ) from tests.components.cloud import mock_cloud +from tests.typing import MockHAClientWebSocket, WebSocketGenerator # Fake webhook thermostat mode change to "Max" FAKE_WEBHOOK = { @@ -82,7 +83,7 @@ async def test_setup_component( mock_impl.assert_called_once() mock_webhook.assert_called_once() - assert config_entry.state is config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) > 0 @@ -392,13 +393,7 @@ async def test_setup_component_invalid_token_scope(hass: HomeAssistant) -> None: "type": "Bearer", "expires_in": 60, "expires_at": time() + 1000, - "scope": " ".join( - [ - "read_smokedetector", - "read_thermostat", - "write_thermostat", - ] - ), + "scope": "read_smokedetector read_thermostat write_thermostat", }, }, options={}, @@ -425,7 +420,7 @@ async def test_setup_component_invalid_token_scope(hass: HomeAssistant) -> None: mock_impl.assert_called_once() mock_webhook.assert_not_called() - assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert hass.config_entries.async_entries(DOMAIN) notifications = async_get_persistent_notifications(hass) @@ -479,7 +474,7 @@ async def test_setup_component_invalid_token( mock_impl.assert_called_once() mock_webhook.assert_not_called() - assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert hass.config_entries.async_entries(DOMAIN) notifications = async_get_persistent_notifications(hass) assert len(notifications) > 0 @@ -520,3 +515,59 @@ async def test_devices( for device_entry in device_entries: identifier = list(device_entry.identifiers)[0] assert device_entry == snapshot(name=f"{identifier[0]}-{identifier[1]}") + + +async def remove_device( + ws_client: MockHAClientWebSocket, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, +) -> None: + """Test we can only remove a device that no longer exists.""" + + assert await async_setup_component(hass, "config", {}) + + with selected_platforms([Platform.CLIMATE]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + climate_entity_livingroom = "climate.livingroom" + entity = entity_registry.async_get(climate_entity_livingroom) + + device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "remove-device-id")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, config_entry.entry_id + ) + is True + ) diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 073b9faf485..4fa64e59b11 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -15,6 +15,7 @@ from .common import selected_platforms, snapshot_platform_entities from tests.common import MockConfigEntry +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -45,8 +46,8 @@ async def test_indoor_sensor( assert hass.states.get(f"{prefix}temperature").state == "20.3" assert hass.states.get(f"{prefix}humidity").state == "63" - assert hass.states.get(f"{prefix}co2").state == "494" - assert hass.states.get(f"{prefix}pressure").state == "1014.5" + assert hass.states.get(f"{prefix}carbon_dioxide").state == "494" + assert hass.states.get(f"{prefix}atmospheric_pressure").state == "1014.5" async def test_weather_sensor( @@ -78,13 +79,13 @@ async def test_public_weather_sensor( assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}humidity").state == "76" - assert hass.states.get(f"{prefix}pressure").state == "1014.4" + assert hass.states.get(f"{prefix}atmospheric_pressure").state == "1014.4" prefix = "sensor.home_avg_" assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}humidity").state == "63.2" - assert hass.states.get(f"{prefix}pressure").state == "1010.4" + assert hass.states.get(f"{prefix}atmospheric_pressure").state == "1010.4" entities_before_change = len(hass.states.async_all()) @@ -135,7 +136,7 @@ async def test_process_rf(strength: int, expected: str) -> None: @pytest.mark.parametrize( ("health", "expected"), - [(4, "Unhealthy"), (3, "Poor"), (2, "Fair"), (1, "Fine"), (0, "Healthy")], + [(4, "unhealthy"), (3, "poor"), (2, "fair"), (1, "fine"), (0, "healthy")], ) async def test_process_health(health: int, expected: str) -> None: """Test health index translation.""" @@ -164,17 +165,17 @@ async def test_process_health(health: int, expected: str) -> None: ), ("12:34:56:80:c1:ea-sum_rain_1", "villa_rain_rain_last_hour", "0"), ("12:34:56:80:c1:ea-sum_rain_24", "villa_rain_rain_today", "6.9"), - ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"), + ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "sw"), ( "12:34:56:03:1b:e4-windangle_value", "netatmoindoor_garden_angle", "217", ), - ("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "S"), + ("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "s"), ( "12:34:56:03:1b:e4-gustangle", "netatmoindoor_garden_gust_direction", - "S", + "s", ), ( "12:34:56:03:1b:e4-gustangle_value", @@ -194,7 +195,7 @@ async def test_process_health(health: int, expected: str) -> None: ( "12:34:56:26:68:92-health_idx", "baby_bedroom_health", - "Fine", + "fine", ), ( "12:34:56:26:68:92-wifi_status", @@ -247,4 +248,4 @@ async def test_climate_battery_sensor( prefix = "sensor.livingroom_" - assert hass.states.get(f"{prefix}battery_percent").state == "75" + assert hass.states.get(f"{prefix}battery").state == "75" diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index c0649d3646e..724a0568580 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import Mock, patch from pynetgear import DEFAULT_USER import pytest -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.netgear.const import ( CONF_CONSIDER_HOME, @@ -23,6 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -83,7 +83,7 @@ async def test_user(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -95,7 +95,7 @@ async def test_user(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -110,7 +110,7 @@ async def test_user_connect_error(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" service.return_value.get_info = Mock(return_value=None) @@ -124,7 +124,7 @@ async def test_user_connect_error(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "info"} @@ -138,7 +138,7 @@ async def test_user_connect_error(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "config"} @@ -148,7 +148,7 @@ async def test_user_incomplete_info(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" router_infos = ROUTER_INFOS.copy() @@ -164,7 +164,7 @@ async def test_user_incomplete_info(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE_INCOMPLETE assert result["data"].get(CONF_HOST) == HOST @@ -186,14 +186,14 @@ async def test_abort_if_already_setup(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -219,7 +219,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -238,7 +238,7 @@ async def test_ssdp_no_serial(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_serial" @@ -264,7 +264,7 @@ async def test_ssdp_ipv6(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -284,13 +284,13 @@ async def test_ssdp(hass: HomeAssistant, service) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -316,7 +316,7 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" service.return_value.port = 5555 @@ -325,7 +325,7 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -350,7 +350,7 @@ async def test_options_flow(hass: HomeAssistant, service) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -360,7 +360,7 @@ async def test_options_flow(hass: HomeAssistant, service) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 1800, } diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py index 71fe8ddb774..6b969e33475 100644 --- a/tests/components/netgear_lte/test_config_flow.py +++ b/tests/components/netgear_lte/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import CONF_DATA @@ -51,7 +52,7 @@ async def test_flow_already_configured( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py index 6a71e6d601c..ef3109123fa 100644 --- a/tests/components/netgear_lte/test_init.py +++ b/tests/components/netgear_lte/test_init.py @@ -13,7 +13,7 @@ from .conftest import CONF_DATA async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None: """Test setup and unload.""" entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -29,7 +29,7 @@ async def test_async_setup_entry_not_ready( """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" entry = hass.config_entries.async_entries(DOMAIN)[0] assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_device( diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 02a3cf06728..6b7956a3bf7 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.nexia.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 @pytest.mark.parametrize("brand", [BRAND_ASAIR, BRAND_NEXIA]) @@ -19,7 +20,7 @@ async def test_form(hass: HomeAssistant, brand) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myhouse" assert result2["data"] == { CONF_BRAND: brand, @@ -76,7 +77,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -99,7 +100,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -124,7 +125,7 @@ async def test_form_invalid_auth_http_401(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -149,7 +150,7 @@ async def test_form_cannot_connect_not_found(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -172,5 +173,5 @@ async def test_form_broad_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nexia/test_init.py b/tests/components/nexia/test_init.py index 8eeb8a9f729..ec84748830a 100644 --- a/tests/components/nexia/test_init.py +++ b/tests/components/nexia/test_init.py @@ -17,7 +17,7 @@ from tests.typing import WebSocketGenerator async def test_setup_retry_client_os_error(hass: HomeAssistant) -> None: """Verify we retry setup on aiohttp.ClientOSError.""" config_entry = await async_init_integration(hass, exception=aiohttp.ClientOSError) - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def remove_device(ws_client, device_id, config_entry_id): diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 466ecb9df61..1af2cff0897 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries, setup from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN -from homeassistant.const import CONF_NAME, CONF_STOP +from homeassistant.const import CONF_STOP from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -29,81 +29,6 @@ def mock_nextbus() -> Generator[MagicMock, None, None]: yield client -async def test_import_config( - hass: HomeAssistant, mock_setup_entry: MagicMock, mock_nextbus_lists: MagicMock -) -> None: - """Test config is imported and component set up.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - data = { - CONF_AGENCY: "sf-muni", - CONF_ROUTE: "F", - CONF_STOP: "5650", - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=data, - ) - await hass.async_block_till_done() - - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert ( - result.get("title") - == "San Francisco Muni F - Market & Wharves Market St & 7th St (Outbound)" - ) - assert result.get("data") == {CONF_NAME: "sf-muni F", **data} - - assert len(mock_setup_entry.mock_calls) == 1 - - # Check duplicate entries are aborted - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=data, - ) - await hass.async_block_till_done() - - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - -@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, - mock_setup_entry: MagicMock, - mock_nextbus_lists: MagicMock, - override: dict[str, str], - expected_reason: str, -) -> None: - """Test user is redirected to user setup flow because they have invalid config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - data = { - CONF_AGENCY: "sf-muni", - CONF_ROUTE: "F", - CONF_STOP: "5650", - **override, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=data, - ) - await hass.async_block_till_done() - - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == expected_reason - - async def test_user_config( hass: HomeAssistant, mock_setup_entry: MagicMock, mock_nextbus_lists: MagicMock ) -> None: @@ -112,7 +37,7 @@ async def test_user_config( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "agency" # Select agency @@ -124,7 +49,7 @@ async def test_user_config( ) await hass.async_block_till_done() - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "route" # Select route @@ -136,7 +61,7 @@ async def test_user_config( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "stop" # Select stop @@ -148,7 +73,7 @@ async def test_user_config( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == { "agency": "sf-muni", "route": "F", diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index ece40b36fb1..5e4f322e1eb 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -5,7 +5,7 @@ from copy import deepcopy from unittest.mock import MagicMock, patch from urllib.error import HTTPError -from py_nextbus.client import NextBusFormatError, NextBusHTTPError, RouteStop +from py_nextbus.client import NextBusFormatError, NextBusHTTPError import pytest from homeassistant.components import sensor @@ -13,10 +13,8 @@ from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMA from homeassistant.components.nextbus.coordinator import NextBusDataUpdateCoordinator from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME, CONF_STOP -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -107,58 +105,6 @@ async def assert_setup_sensor( return config_entry -async def test_legacy_yaml_setup( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test config setup and yaml deprecation.""" - with patch( - "homeassistant.components.nextbus.config_flow.NextBusClient", - ) as NextBusClient: - NextBusClient.return_value.get_predictions_for_multi_stops.return_value = ( - BASIC_RESULTS - ) - await async_setup_component(hass, sensor.DOMAIN, PLATFORM_CONFIG) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue - - -async def test_valid_config( - hass: HomeAssistant, mock_nextbus: MagicMock, mock_nextbus_lists: MagicMock -) -> None: - """Test that sensor is set up properly with valid config.""" - await assert_setup_sensor(hass, CONFIG_BASIC) - - -async def test_verify_valid_state( - hass: HomeAssistant, - mock_nextbus: MagicMock, - mock_nextbus_lists: MagicMock, - mock_nextbus_predictions: MagicMock, -) -> None: - """Verify all attributes are set from a valid response.""" - await assert_setup_sensor(hass, CONFIG_BASIC) - entity = er.async_get(hass).async_get(SENSOR_ID) - assert entity - - mock_nextbus_predictions.assert_called_once_with( - {RouteStop(VALID_ROUTE, VALID_STOP)} - ) - - state = hass.states.get(SENSOR_ID) - assert state is not None - assert state.state == "2019-03-28T21:09:31+00:00" - assert state.attributes["agency"] == VALID_AGENCY_TITLE - assert state.attributes["route"] == VALID_ROUTE_TITLE - assert state.attributes["stop"] == VALID_STOP_TITLE - assert state.attributes["direction"] == "Outbound" - assert state.attributes["upcoming"] == "1, 2, 3, 10" - - async def test_message_dict( hass: HomeAssistant, mock_nextbus: MagicMock, diff --git a/tests/components/nextcloud/conftest.py b/tests/components/nextcloud/conftest.py index 0ea281abb49..58b37359d42 100644 --- a/tests/components/nextcloud/conftest.py +++ b/tests/components/nextcloud/conftest.py @@ -9,12 +9,10 @@ import pytest @pytest.fixture def mock_nextcloud_monitor() -> Mock: """Mock of NextcloudMonitor.""" - ncm = Mock( + return Mock( update=Mock(return_value=True), ) - return ncm - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: diff --git a/tests/components/nextcloud/test_config_flow.py b/tests/components/nextcloud/test_config_flow.py index 32c688fb8c2..9a881197cf9 100644 --- a/tests/components/nextcloud/test_config_flow.py +++ b/tests/components/nextcloud/test_config_flow.py @@ -36,7 +36,7 @@ async def test_user_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -50,7 +50,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -64,7 +64,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -78,7 +78,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -93,7 +93,7 @@ async def test_user_create_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "nc_url" assert result["data"] == snapshot @@ -113,7 +113,7 @@ async def test_user_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -127,7 +127,7 @@ async def test_user_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_reauth( context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # test NextcloudMonitorAuthorizationError @@ -165,7 +165,7 @@ async def test_reauth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -182,7 +182,7 @@ async def test_reauth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "connection_error"} @@ -199,7 +199,7 @@ async def test_reauth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "connection_error"} @@ -217,6 +217,6 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == snapshot diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index e4948a9358f..4cf74d72e63 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -1,5 +1,6 @@ """Tests for the NextDNS integration.""" +from contextlib import contextmanager from unittest.mock import patch from nextdns import ( @@ -113,16 +114,9 @@ SETTINGS = Settings( ) -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: - """Set up the NextDNS integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Fake Profile", - unique_id="xyz12", - data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, - entry_id="d9aa37407ddac7b964a99e86312288d6", - ) - +@contextmanager +def mock_nextdns(): + """Mock the NextDNS class.""" with ( patch( "homeassistant.components.nextdns.NextDns.get_profiles", @@ -157,7 +151,22 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: return_value=CONNECTION_STATUS, ), ): - entry.add_to_hass(hass) + yield + + +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the NextDNS integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + entry_id="d9aa37407ddac7b964a99e86312288d6", + ) + + entry.add_to_hass(hass) + + with mock_nextdns(): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/nextdns/snapshots/test_binary_sensor.ambr b/tests/components/nextdns/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..bd4ecbba084 --- /dev/null +++ b/tests/components/nextdns/snapshots/test_binary_sensor.ambr @@ -0,0 +1,2277 @@ +# serializer version: 1 +# name: test_binary_Sensor[switch.fake_profile_ai_driven_threat_detection-entry] + 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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_allow_affiliate_tracking_links-entry] + 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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_anonymized_edns_client_subnet-entry] + 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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_9gag-entry] + 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.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_bypass_methods-entry] + 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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_child_sexual_abuse_material-entry] + 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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_disguised_third_party_trackers-entry] + 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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_dynamic_dns_hostnames-entry] + 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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_newly_registered_domains-entry] + 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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_page-entry] + 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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_parked_domains-entry] + 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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cache_boost-entry] + 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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cname_flattening-entry] + 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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cryptojacking_protection-entry] + 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.fake_profile_cryptojacking_protection', + '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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_dns_rebinding_protection-entry] + 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.fake_profile_dns_rebinding_protection', + '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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_domain_generation_algorithms_protection-entry] + 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.fake_profile_domain_generation_algorithms_protection', + '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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_safesearch-entry] + 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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_youtube_restricted_mode-entry] + 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.fake_profile_force_youtube_restricted_mode', + '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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_google_safe_browsing-entry] + 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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_idn_homograph_attacks_protection-entry] + 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.fake_profile_idn_homograph_attacks_protection', + '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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_logs-entry] + 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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_threat_intelligence_feeds-entry] + 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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_typosquatting_protection-entry] + 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.fake_profile_typosquatting_protection', + '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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_web3-entry] + 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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_connection_status-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': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_connection_status', + 'unique_id': 'xyz12_this_device_nextdns_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_profile_connection_status-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': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device profile connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_profile_connection_status', + 'unique_id': 'xyz12_this_device_profile_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_profile_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device profile connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_ai_driven_threat_detection-entry] + 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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_allow_affiliate_tracking_links-entry] + 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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_anonymized_edns_client_subnet-entry] + 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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_9gag-entry] + 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.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_bypass_methods-entry] + 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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_child_sexual_abuse_material-entry] + 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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_disguised_third_party_trackers-entry] + 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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_dynamic_dns_hostnames-entry] + 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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_newly_registered_domains-entry] + 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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_page-entry] + 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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_parked_domains-entry] + 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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cache_boost-entry] + 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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cname_flattening-entry] + 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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cryptojacking_protection-entry] + 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.fake_profile_cryptojacking_protection', + '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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_dns_rebinding_protection-entry] + 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.fake_profile_dns_rebinding_protection', + '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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_domain_generation_algorithms_protection-entry] + 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.fake_profile_domain_generation_algorithms_protection', + '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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_safesearch-entry] + 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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_youtube_restricted_mode-entry] + 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.fake_profile_force_youtube_restricted_mode', + '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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_google_safe_browsing-entry] + 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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_idn_homograph_attacks_protection-entry] + 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.fake_profile_idn_homograph_attacks_protection', + '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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_logs-entry] + 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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_threat_intelligence_feeds-entry] + 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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_typosquatting_protection-entry] + 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.fake_profile_typosquatting_protection', + '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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_web3-entry] + 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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nextdns/snapshots/test_button.ambr b/tests/components/nextdns/snapshots/test_button.ambr new file mode 100644 index 00000000000..32dc31eea19 --- /dev/null +++ b/tests/components/nextdns/snapshots/test_button.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_button[button.fake_profile_clear_logs-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.fake_profile_clear_logs', + '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': 'Clear logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clear_logs', + 'unique_id': 'xyz12_clear_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[button.fake_profile_clear_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Clear logs', + }), + 'context': , + 'entity_id': 'button.fake_profile_clear_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/nextdns/snapshots/test_sensor.ambr b/tests/components/nextdns/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..34b40433e3b --- /dev/null +++ b/tests/components/nextdns/snapshots/test_sensor.ambr @@ -0,0 +1,4749 @@ +# serializer version: 1 +# name: test_sensor[binary_sensor.fake_profile_device_connection_status-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': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_connection_status', + 'unique_id': 'xyz12_this_device_nextdns_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.fake_profile_device_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[binary_sensor.fake_profile_device_profile_connection_status-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': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device profile connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_profile_connection_status', + 'unique_id': 'xyz12_this_device_profile_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.fake_profile_device_profile_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device profile connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[button.fake_profile_clear_logs-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.fake_profile_clear_logs', + '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': 'Clear logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clear_logs', + 'unique_id': 'xyz12_clear_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[button.fake_profile_clear_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Clear logs', + }), + 'context': , + 'entity_id': 'button.fake_profile_clear_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries-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.fake_profile_dns_over_http_3_queries', + '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': 'DNS-over-HTTP/3 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries', + 'unique_id': 'xyz12_doh3_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries_ratio-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.fake_profile_dns_over_http_3_queries_ratio', + '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': 'DNS-over-HTTP/3 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries_ratio', + 'unique_id': 'xyz12_doh3_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries-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.fake_profile_dns_over_https_queries', + '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': 'DNS-over-HTTPS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries', + 'unique_id': 'xyz12_doh_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries_ratio-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.fake_profile_dns_over_https_queries_ratio', + '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': 'DNS-over-HTTPS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries_ratio', + 'unique_id': 'xyz12_doh_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.4', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries-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.fake_profile_dns_over_quic_queries', + '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': 'DNS-over-QUIC queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries', + 'unique_id': 'xyz12_doq_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries_ratio-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.fake_profile_dns_over_quic_queries_ratio', + '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': 'DNS-over-QUIC queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries_ratio', + 'unique_id': 'xyz12_doq_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.7', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries-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.fake_profile_dns_over_tls_queries', + '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': 'DNS-over-TLS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries', + 'unique_id': 'xyz12_dot_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries_ratio-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.fake_profile_dns_over_tls_queries_ratio', + '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': 'DNS-over-TLS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries_ratio', + 'unique_id': 'xyz12_dot_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.1', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries-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.fake_profile_dns_queries', + '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': 'DNS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'all_queries', + 'unique_id': 'xyz12_all_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked-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.fake_profile_dns_queries_blocked', + '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': 'DNS queries blocked', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries', + 'unique_id': 'xyz12_blocked_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked_ratio-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.fake_profile_dns_queries_blocked_ratio', + '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': 'DNS queries blocked ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries_ratio', + 'unique_id': 'xyz12_blocked_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_relayed-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.fake_profile_dns_queries_relayed', + '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': 'DNS queries relayed', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relayed_queries', + 'unique_id': 'xyz12_relayed_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_relayed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries relayed', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_relayed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_not_validated_queries-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.fake_profile_dnssec_not_validated_queries', + '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': 'DNSSEC not validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'not_validated_queries', + 'unique_id': 'xyz12_not_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_not_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC not validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_not_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries-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.fake_profile_dnssec_validated_queries', + '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': 'DNSSEC validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries', + 'unique_id': 'xyz12_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries_ratio-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.fake_profile_dnssec_validated_queries_ratio', + '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': 'DNSSEC validated queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries_ratio', + 'unique_id': 'xyz12_validated_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries-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.fake_profile_encrypted_queries', + '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': 'Encrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries', + 'unique_id': 'xyz12_encrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries_ratio-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.fake_profile_encrypted_queries_ratio', + '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': 'Encrypted queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries_ratio', + 'unique_id': 'xyz12_encrypted_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv4_queries-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.fake_profile_ipv4_queries', + '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': 'IPv4 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv4_queries', + 'unique_id': 'xyz12_ipv4_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv4_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv4 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv4_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries-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.fake_profile_ipv6_queries', + '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': 'IPv6 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries', + 'unique_id': 'xyz12_ipv6_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries_ratio-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.fake_profile_ipv6_queries_ratio', + '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': 'IPv6 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries_ratio', + 'unique_id': 'xyz12_ipv6_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries-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.fake_profile_tcp_queries', + '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': 'TCP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries', + 'unique_id': 'xyz12_tcp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries_ratio-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.fake_profile_tcp_queries_ratio', + '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': 'TCP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries_ratio', + 'unique_id': 'xyz12_tcp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries-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.fake_profile_udp_queries', + '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': 'UDP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries', + 'unique_id': 'xyz12_udp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries_ratio-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.fake_profile_udp_queries_ratio', + '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': 'UDP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries_ratio', + 'unique_id': 'xyz12_udp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.8', + }) +# --- +# name: test_sensor[sensor.fake_profile_unencrypted_queries-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.fake_profile_unencrypted_queries', + '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': 'Unencrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'unencrypted_queries', + 'unique_id': 'xyz12_unencrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_unencrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Unencrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_unencrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensor[switch.fake_profile_ai_driven_threat_detection-entry] + 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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_allow_affiliate_tracking_links-entry] + 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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_anonymized_edns_client_subnet-entry] + 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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_9gag-entry] + 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.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_9gag-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block 9GAG', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_9gag', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_amazon-entry] + 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.fake_profile_block_amazon', + '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': 'Block Amazon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_amazon', + 'unique_id': 'xyz12_block_amazon', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_amazon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Amazon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_amazon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_bereal-entry] + 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.fake_profile_block_bereal', + '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': 'Block BeReal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bereal', + 'unique_id': 'xyz12_block_bereal', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_bereal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block BeReal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bereal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_blizzard-entry] + 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.fake_profile_block_blizzard', + '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': 'Block Blizzard', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_blizzard', + 'unique_id': 'xyz12_block_blizzard', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_blizzard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Blizzard', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_blizzard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_bypass_methods-entry] + 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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_chatgpt-entry] + 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.fake_profile_block_chatgpt', + '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': 'Block ChatGPT', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_chatgpt', + 'unique_id': 'xyz12_block_chatgpt', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_chatgpt-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block ChatGPT', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_chatgpt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_child_sexual_abuse_material-entry] + 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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_dailymotion-entry] + 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.fake_profile_block_dailymotion', + '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': 'Block Dailymotion', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dailymotion', + 'unique_id': 'xyz12_block_dailymotion', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_dailymotion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Dailymotion', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dailymotion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_dating-entry] + 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.fake_profile_block_dating', + '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': 'Block dating', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dating', + 'unique_id': 'xyz12_block_dating', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_dating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dating', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_discord-entry] + 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.fake_profile_block_discord', + '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': 'Block Discord', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_discord', + 'unique_id': 'xyz12_block_discord', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_discord-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Discord', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_discord', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_disguised_third_party_trackers-entry] + 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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_disney_plus-entry] + 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.fake_profile_block_disney_plus', + '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': 'Block Disney Plus', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disneyplus', + 'unique_id': 'xyz12_block_disneyplus', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_disney_plus-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Disney Plus', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disney_plus', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_dynamic_dns_hostnames-entry] + 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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_ebay-entry] + 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.fake_profile_block_ebay', + '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': 'Block eBay', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ebay', + 'unique_id': 'xyz12_block_ebay', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_ebay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block eBay', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_ebay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_facebook-entry] + 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.fake_profile_block_facebook', + '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': 'Block Facebook', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_facebook', + 'unique_id': 'xyz12_block_facebook', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_facebook-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Facebook', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_facebook', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_fortnite-entry] + 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.fake_profile_block_fortnite', + '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': 'Block Fortnite', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_fortnite', + 'unique_id': 'xyz12_block_fortnite', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_fortnite-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Fortnite', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_fortnite', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_gambling-entry] + 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.fake_profile_block_gambling', + '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': 'Block gambling', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_gambling', + 'unique_id': 'xyz12_block_gambling', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_gambling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block gambling', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_gambling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_google_chat-entry] + 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.fake_profile_block_google_chat', + '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': 'Block Google Chat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_google_chat', + 'unique_id': 'xyz12_block_google_chat', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_google_chat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Google Chat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_google_chat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_hbo_max-entry] + 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.fake_profile_block_hbo_max', + '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': 'Block HBO Max', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_hbomax', + 'unique_id': 'xyz12_block_hbomax', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_hbo_max-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block HBO Max', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hbo_max', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_hulu-entry] + 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.fake_profile_block_hulu', + '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': 'Block Hulu', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xyz12_block_hulu', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_hulu-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Hulu', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hulu', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_imgur-entry] + 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.fake_profile_block_imgur', + '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': 'Block Imgur', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_imgur', + 'unique_id': 'xyz12_block_imgur', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_imgur-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Imgur', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_imgur', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_instagram-entry] + 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.fake_profile_block_instagram', + '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': 'Block Instagram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_instagram', + 'unique_id': 'xyz12_block_instagram', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_instagram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Instagram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_instagram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_league_of_legends-entry] + 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.fake_profile_block_league_of_legends', + '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': 'Block League of Legends', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_leagueoflegends', + 'unique_id': 'xyz12_block_leagueoflegends', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_league_of_legends-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block League of Legends', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_league_of_legends', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_mastodon-entry] + 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.fake_profile_block_mastodon', + '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': 'Block Mastodon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_mastodon', + 'unique_id': 'xyz12_block_mastodon', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_mastodon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Mastodon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_mastodon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_messenger-entry] + 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.fake_profile_block_messenger', + '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': 'Block Messenger', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_messenger', + 'unique_id': 'xyz12_block_messenger', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_messenger-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Messenger', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_messenger', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_minecraft-entry] + 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.fake_profile_block_minecraft', + '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': 'Block Minecraft', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_minecraft', + 'unique_id': 'xyz12_block_minecraft', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_minecraft-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Minecraft', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_minecraft', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_netflix-entry] + 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.fake_profile_block_netflix', + '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': 'Block Netflix', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_netflix', + 'unique_id': 'xyz12_block_netflix', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_netflix-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Netflix', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_netflix', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_newly_registered_domains-entry] + 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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_online_gaming-entry] + 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.fake_profile_block_online_gaming', + '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': 'Block online gaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_online_gaming', + 'unique_id': 'xyz12_block_online_gaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_online_gaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block online gaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_online_gaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_page-entry] + 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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_block_parked_domains-entry] + 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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_pinterest-entry] + 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.fake_profile_block_pinterest', + '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': 'Block Pinterest', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_pinterest', + 'unique_id': 'xyz12_block_pinterest', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_pinterest-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Pinterest', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_pinterest', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_piracy-entry] + 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.fake_profile_block_piracy', + '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': 'Block piracy', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_piracy', + 'unique_id': 'xyz12_block_piracy', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_piracy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block piracy', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_piracy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_playstation_network-entry] + 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.fake_profile_block_playstation_network', + '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': 'Block PlayStation Network', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_playstation_network', + 'unique_id': 'xyz12_block_playstation_network', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_playstation_network-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block PlayStation Network', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_playstation_network', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_porn-entry] + 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.fake_profile_block_porn', + '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': 'Block porn', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_porn', + 'unique_id': 'xyz12_block_porn', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_porn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block porn', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_porn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_prime_video-entry] + 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.fake_profile_block_prime_video', + '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': 'Block Prime Video', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_primevideo', + 'unique_id': 'xyz12_block_primevideo', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_prime_video-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Prime Video', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_prime_video', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_reddit-entry] + 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.fake_profile_block_reddit', + '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': 'Block Reddit', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_reddit', + 'unique_id': 'xyz12_block_reddit', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_reddit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Reddit', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_reddit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_roblox-entry] + 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.fake_profile_block_roblox', + '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': 'Block Roblox', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_roblox', + 'unique_id': 'xyz12_block_roblox', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_roblox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Roblox', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_roblox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_signal-entry] + 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.fake_profile_block_signal', + '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': 'Block Signal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_signal', + 'unique_id': 'xyz12_block_signal', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_signal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Signal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_signal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_skype-entry] + 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.fake_profile_block_skype', + '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': 'Block Skype', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_skype', + 'unique_id': 'xyz12_block_skype', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_skype-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Skype', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_skype', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_snapchat-entry] + 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.fake_profile_block_snapchat', + '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': 'Block Snapchat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_snapchat', + 'unique_id': 'xyz12_block_snapchat', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_snapchat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Snapchat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_snapchat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_social_networks-entry] + 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.fake_profile_block_social_networks', + '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': 'Block social networks', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_social_networks', + 'unique_id': 'xyz12_block_social_networks', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_social_networks-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block social networks', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_social_networks', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_spotify-entry] + 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.fake_profile_block_spotify', + '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': 'Block Spotify', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_spotify', + 'unique_id': 'xyz12_block_spotify', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_spotify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Spotify', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_spotify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_steam-entry] + 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.fake_profile_block_steam', + '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': 'Block Steam', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_steam', + 'unique_id': 'xyz12_block_steam', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_steam-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Steam', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_steam', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_telegram-entry] + 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.fake_profile_block_telegram', + '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': 'Block Telegram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_telegram', + 'unique_id': 'xyz12_block_telegram', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_telegram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Telegram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_telegram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_tiktok-entry] + 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.fake_profile_block_tiktok', + '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': 'Block TikTok', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tiktok', + 'unique_id': 'xyz12_block_tiktok', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_tiktok-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block TikTok', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tiktok', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_tinder-entry] + 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.fake_profile_block_tinder', + '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': 'Block Tinder', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tinder', + 'unique_id': 'xyz12_block_tinder', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_tinder-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tinder', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tinder', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_tumblr-entry] + 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.fake_profile_block_tumblr', + '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': 'Block Tumblr', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tumblr', + 'unique_id': 'xyz12_block_tumblr', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_tumblr-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tumblr', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tumblr', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_twitch-entry] + 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.fake_profile_block_twitch', + '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': 'Block Twitch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitch', + 'unique_id': 'xyz12_block_twitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_twitch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Twitch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_twitch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_video_streaming-entry] + 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.fake_profile_block_video_streaming', + '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': 'Block video streaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_video_streaming', + 'unique_id': 'xyz12_block_video_streaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_video_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block video streaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_video_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_vimeo-entry] + 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.fake_profile_block_vimeo', + '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': 'Block Vimeo', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vimeo', + 'unique_id': 'xyz12_block_vimeo', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_vimeo-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Vimeo', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vimeo', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_vk-entry] + 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.fake_profile_block_vk', + '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': 'Block VK', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vk', + 'unique_id': 'xyz12_block_vk', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_vk-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block VK', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vk', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_whatsapp-entry] + 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.fake_profile_block_whatsapp', + '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': 'Block WhatsApp', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_whatsapp', + 'unique_id': 'xyz12_block_whatsapp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_whatsapp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block WhatsApp', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_whatsapp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_x_formerly_twitter-entry] + 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.fake_profile_block_x_formerly_twitter', + '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': 'Block X (formerly Twitter)', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitter', + 'unique_id': 'xyz12_block_twitter', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_x_formerly_twitter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block X (formerly Twitter)', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_x_formerly_twitter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_xbox_live-entry] + 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.fake_profile_block_xbox_live', + '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': 'Block Xbox Live', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_xboxlive', + 'unique_id': 'xyz12_block_xboxlive', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_xbox_live-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Xbox Live', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_xbox_live', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_youtube-entry] + 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.fake_profile_block_youtube', + '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': 'Block YouTube', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_youtube', + 'unique_id': 'xyz12_block_youtube', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_youtube-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block YouTube', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_youtube', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_zoom-entry] + 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.fake_profile_block_zoom', + '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': 'Block Zoom', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_zoom', + 'unique_id': 'xyz12_block_zoom', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_zoom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Zoom', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_zoom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_cache_boost-entry] + 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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_cname_flattening-entry] + 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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_cryptojacking_protection-entry] + 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.fake_profile_cryptojacking_protection', + '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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_dns_rebinding_protection-entry] + 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.fake_profile_dns_rebinding_protection', + '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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_domain_generation_algorithms_protection-entry] + 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.fake_profile_domain_generation_algorithms_protection', + '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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_force_safesearch-entry] + 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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_force_youtube_restricted_mode-entry] + 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.fake_profile_force_youtube_restricted_mode', + '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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_google_safe_browsing-entry] + 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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_idn_homograph_attacks_protection-entry] + 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.fake_profile_idn_homograph_attacks_protection', + '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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_logs-entry] + 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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_threat_intelligence_feeds-entry] + 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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_typosquatting_protection-entry] + 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.fake_profile_typosquatting_protection', + '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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_web3-entry] + 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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nextdns/snapshots/test_switch.ambr b/tests/components/nextdns/snapshots/test_switch.ambr new file mode 100644 index 00000000000..8472f02e8c5 --- /dev/null +++ b/tests/components/nextdns/snapshots/test_switch.ambr @@ -0,0 +1,4749 @@ +# serializer version: 1 +# name: test_switch[binary_sensor.fake_profile_device_connection_status-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': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_connection_status', + 'unique_id': 'xyz12_this_device_nextdns_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[binary_sensor.fake_profile_device_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[binary_sensor.fake_profile_device_profile_connection_status-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': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device profile connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_profile_connection_status', + 'unique_id': 'xyz12_this_device_profile_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[binary_sensor.fake_profile_device_profile_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device profile connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[button.fake_profile_clear_logs-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.fake_profile_clear_logs', + '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': 'Clear logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clear_logs', + 'unique_id': 'xyz12_clear_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[button.fake_profile_clear_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Clear logs', + }), + 'context': , + 'entity_id': 'button.fake_profile_clear_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries-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.fake_profile_dns_over_http_3_queries', + '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': 'DNS-over-HTTP/3 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries', + 'unique_id': 'xyz12_doh3_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries_ratio-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.fake_profile_dns_over_http_3_queries_ratio', + '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': 'DNS-over-HTTP/3 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries_ratio', + 'unique_id': 'xyz12_doh3_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries-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.fake_profile_dns_over_https_queries', + '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': 'DNS-over-HTTPS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries', + 'unique_id': 'xyz12_doh_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries_ratio-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.fake_profile_dns_over_https_queries_ratio', + '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': 'DNS-over-HTTPS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries_ratio', + 'unique_id': 'xyz12_doh_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.4', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries-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.fake_profile_dns_over_quic_queries', + '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': 'DNS-over-QUIC queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries', + 'unique_id': 'xyz12_doq_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries_ratio-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.fake_profile_dns_over_quic_queries_ratio', + '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': 'DNS-over-QUIC queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries_ratio', + 'unique_id': 'xyz12_doq_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.7', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries-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.fake_profile_dns_over_tls_queries', + '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': 'DNS-over-TLS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries', + 'unique_id': 'xyz12_dot_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries_ratio-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.fake_profile_dns_over_tls_queries_ratio', + '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': 'DNS-over-TLS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries_ratio', + 'unique_id': 'xyz12_dot_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.1', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries-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.fake_profile_dns_queries', + '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': 'DNS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'all_queries', + 'unique_id': 'xyz12_all_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked-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.fake_profile_dns_queries_blocked', + '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': 'DNS queries blocked', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries', + 'unique_id': 'xyz12_blocked_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked_ratio-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.fake_profile_dns_queries_blocked_ratio', + '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': 'DNS queries blocked ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries_ratio', + 'unique_id': 'xyz12_blocked_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_relayed-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.fake_profile_dns_queries_relayed', + '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': 'DNS queries relayed', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relayed_queries', + 'unique_id': 'xyz12_relayed_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_relayed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries relayed', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_relayed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_not_validated_queries-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.fake_profile_dnssec_not_validated_queries', + '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': 'DNSSEC not validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'not_validated_queries', + 'unique_id': 'xyz12_not_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_not_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC not validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_not_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries-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.fake_profile_dnssec_validated_queries', + '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': 'DNSSEC validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries', + 'unique_id': 'xyz12_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries_ratio-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.fake_profile_dnssec_validated_queries_ratio', + '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': 'DNSSEC validated queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries_ratio', + 'unique_id': 'xyz12_validated_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.0', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries-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.fake_profile_encrypted_queries', + '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': 'Encrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries', + 'unique_id': 'xyz12_encrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries_ratio-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.fake_profile_encrypted_queries_ratio', + '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': 'Encrypted queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries_ratio', + 'unique_id': 'xyz12_encrypted_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv4_queries-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.fake_profile_ipv4_queries', + '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': 'IPv4 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv4_queries', + 'unique_id': 'xyz12_ipv4_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv4_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv4 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv4_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries-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.fake_profile_ipv6_queries', + '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': 'IPv6 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries', + 'unique_id': 'xyz12_ipv6_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries_ratio-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.fake_profile_ipv6_queries_ratio', + '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': 'IPv6 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries_ratio', + 'unique_id': 'xyz12_ipv6_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries-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.fake_profile_tcp_queries', + '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': 'TCP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries', + 'unique_id': 'xyz12_tcp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries_ratio-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.fake_profile_tcp_queries_ratio', + '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': 'TCP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries_ratio', + 'unique_id': 'xyz12_tcp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries-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.fake_profile_udp_queries', + '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': 'UDP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries', + 'unique_id': 'xyz12_udp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries_ratio-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.fake_profile_udp_queries_ratio', + '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': 'UDP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries_ratio', + 'unique_id': 'xyz12_udp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.8', + }) +# --- +# name: test_switch[sensor.fake_profile_unencrypted_queries-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.fake_profile_unencrypted_queries', + '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': 'Unencrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'unencrypted_queries', + 'unique_id': 'xyz12_unencrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_unencrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Unencrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_unencrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_switch[switch.fake_profile_ai_driven_threat_detection-entry] + 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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_allow_affiliate_tracking_links-entry] + 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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_anonymized_edns_client_subnet-entry] + 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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_9gag-entry] + 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.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_9gag-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block 9GAG', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_9gag', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_amazon-entry] + 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.fake_profile_block_amazon', + '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': 'Block Amazon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_amazon', + 'unique_id': 'xyz12_block_amazon', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_amazon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Amazon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_amazon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_bereal-entry] + 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.fake_profile_block_bereal', + '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': 'Block BeReal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bereal', + 'unique_id': 'xyz12_block_bereal', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_bereal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block BeReal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bereal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_blizzard-entry] + 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.fake_profile_block_blizzard', + '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': 'Block Blizzard', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_blizzard', + 'unique_id': 'xyz12_block_blizzard', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_blizzard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Blizzard', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_blizzard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_bypass_methods-entry] + 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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_chatgpt-entry] + 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.fake_profile_block_chatgpt', + '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': 'Block ChatGPT', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_chatgpt', + 'unique_id': 'xyz12_block_chatgpt', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_chatgpt-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block ChatGPT', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_chatgpt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_child_sexual_abuse_material-entry] + 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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_dailymotion-entry] + 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.fake_profile_block_dailymotion', + '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': 'Block Dailymotion', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dailymotion', + 'unique_id': 'xyz12_block_dailymotion', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_dailymotion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Dailymotion', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dailymotion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_dating-entry] + 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.fake_profile_block_dating', + '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': 'Block dating', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dating', + 'unique_id': 'xyz12_block_dating', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_dating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dating', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_discord-entry] + 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.fake_profile_block_discord', + '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': 'Block Discord', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_discord', + 'unique_id': 'xyz12_block_discord', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_discord-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Discord', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_discord', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_disguised_third_party_trackers-entry] + 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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_disney_plus-entry] + 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.fake_profile_block_disney_plus', + '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': 'Block Disney Plus', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disneyplus', + 'unique_id': 'xyz12_block_disneyplus', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_disney_plus-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Disney Plus', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disney_plus', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_dynamic_dns_hostnames-entry] + 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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_ebay-entry] + 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.fake_profile_block_ebay', + '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': 'Block eBay', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ebay', + 'unique_id': 'xyz12_block_ebay', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_ebay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block eBay', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_ebay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_facebook-entry] + 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.fake_profile_block_facebook', + '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': 'Block Facebook', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_facebook', + 'unique_id': 'xyz12_block_facebook', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_facebook-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Facebook', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_facebook', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_fortnite-entry] + 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.fake_profile_block_fortnite', + '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': 'Block Fortnite', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_fortnite', + 'unique_id': 'xyz12_block_fortnite', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_fortnite-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Fortnite', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_fortnite', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_gambling-entry] + 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.fake_profile_block_gambling', + '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': 'Block gambling', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_gambling', + 'unique_id': 'xyz12_block_gambling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_gambling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block gambling', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_gambling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_google_chat-entry] + 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.fake_profile_block_google_chat', + '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': 'Block Google Chat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_google_chat', + 'unique_id': 'xyz12_block_google_chat', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_google_chat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Google Chat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_google_chat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_hbo_max-entry] + 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.fake_profile_block_hbo_max', + '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': 'Block HBO Max', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_hbomax', + 'unique_id': 'xyz12_block_hbomax', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_hbo_max-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block HBO Max', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hbo_max', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_hulu-entry] + 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.fake_profile_block_hulu', + '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': 'Block Hulu', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xyz12_block_hulu', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_hulu-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Hulu', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hulu', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_imgur-entry] + 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.fake_profile_block_imgur', + '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': 'Block Imgur', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_imgur', + 'unique_id': 'xyz12_block_imgur', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_imgur-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Imgur', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_imgur', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_instagram-entry] + 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.fake_profile_block_instagram', + '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': 'Block Instagram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_instagram', + 'unique_id': 'xyz12_block_instagram', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_instagram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Instagram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_instagram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_league_of_legends-entry] + 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.fake_profile_block_league_of_legends', + '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': 'Block League of Legends', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_leagueoflegends', + 'unique_id': 'xyz12_block_leagueoflegends', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_league_of_legends-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block League of Legends', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_league_of_legends', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_mastodon-entry] + 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.fake_profile_block_mastodon', + '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': 'Block Mastodon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_mastodon', + 'unique_id': 'xyz12_block_mastodon', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_mastodon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Mastodon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_mastodon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_messenger-entry] + 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.fake_profile_block_messenger', + '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': 'Block Messenger', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_messenger', + 'unique_id': 'xyz12_block_messenger', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_messenger-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Messenger', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_messenger', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_minecraft-entry] + 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.fake_profile_block_minecraft', + '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': 'Block Minecraft', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_minecraft', + 'unique_id': 'xyz12_block_minecraft', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_minecraft-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Minecraft', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_minecraft', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_netflix-entry] + 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.fake_profile_block_netflix', + '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': 'Block Netflix', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_netflix', + 'unique_id': 'xyz12_block_netflix', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_netflix-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Netflix', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_netflix', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_newly_registered_domains-entry] + 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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_online_gaming-entry] + 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.fake_profile_block_online_gaming', + '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': 'Block online gaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_online_gaming', + 'unique_id': 'xyz12_block_online_gaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_online_gaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block online gaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_online_gaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_page-entry] + 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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_block_parked_domains-entry] + 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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_pinterest-entry] + 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.fake_profile_block_pinterest', + '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': 'Block Pinterest', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_pinterest', + 'unique_id': 'xyz12_block_pinterest', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_pinterest-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Pinterest', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_pinterest', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_piracy-entry] + 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.fake_profile_block_piracy', + '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': 'Block piracy', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_piracy', + 'unique_id': 'xyz12_block_piracy', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_piracy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block piracy', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_piracy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_playstation_network-entry] + 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.fake_profile_block_playstation_network', + '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': 'Block PlayStation Network', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_playstation_network', + 'unique_id': 'xyz12_block_playstation_network', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_playstation_network-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block PlayStation Network', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_playstation_network', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_porn-entry] + 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.fake_profile_block_porn', + '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': 'Block porn', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_porn', + 'unique_id': 'xyz12_block_porn', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_porn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block porn', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_porn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_prime_video-entry] + 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.fake_profile_block_prime_video', + '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': 'Block Prime Video', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_primevideo', + 'unique_id': 'xyz12_block_primevideo', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_prime_video-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Prime Video', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_prime_video', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_reddit-entry] + 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.fake_profile_block_reddit', + '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': 'Block Reddit', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_reddit', + 'unique_id': 'xyz12_block_reddit', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_reddit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Reddit', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_reddit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_roblox-entry] + 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.fake_profile_block_roblox', + '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': 'Block Roblox', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_roblox', + 'unique_id': 'xyz12_block_roblox', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_roblox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Roblox', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_roblox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_signal-entry] + 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.fake_profile_block_signal', + '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': 'Block Signal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_signal', + 'unique_id': 'xyz12_block_signal', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_signal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Signal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_signal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_skype-entry] + 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.fake_profile_block_skype', + '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': 'Block Skype', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_skype', + 'unique_id': 'xyz12_block_skype', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_skype-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Skype', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_skype', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_snapchat-entry] + 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.fake_profile_block_snapchat', + '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': 'Block Snapchat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_snapchat', + 'unique_id': 'xyz12_block_snapchat', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_snapchat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Snapchat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_snapchat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_social_networks-entry] + 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.fake_profile_block_social_networks', + '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': 'Block social networks', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_social_networks', + 'unique_id': 'xyz12_block_social_networks', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_social_networks-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block social networks', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_social_networks', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_spotify-entry] + 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.fake_profile_block_spotify', + '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': 'Block Spotify', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_spotify', + 'unique_id': 'xyz12_block_spotify', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_spotify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Spotify', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_spotify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_steam-entry] + 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.fake_profile_block_steam', + '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': 'Block Steam', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_steam', + 'unique_id': 'xyz12_block_steam', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_steam-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Steam', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_steam', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_telegram-entry] + 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.fake_profile_block_telegram', + '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': 'Block Telegram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_telegram', + 'unique_id': 'xyz12_block_telegram', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_telegram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Telegram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_telegram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_tiktok-entry] + 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.fake_profile_block_tiktok', + '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': 'Block TikTok', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tiktok', + 'unique_id': 'xyz12_block_tiktok', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_tiktok-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block TikTok', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tiktok', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_tinder-entry] + 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.fake_profile_block_tinder', + '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': 'Block Tinder', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tinder', + 'unique_id': 'xyz12_block_tinder', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_tinder-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tinder', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tinder', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_tumblr-entry] + 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.fake_profile_block_tumblr', + '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': 'Block Tumblr', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tumblr', + 'unique_id': 'xyz12_block_tumblr', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_tumblr-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tumblr', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tumblr', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_twitch-entry] + 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.fake_profile_block_twitch', + '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': 'Block Twitch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitch', + 'unique_id': 'xyz12_block_twitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_twitch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Twitch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_twitch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_video_streaming-entry] + 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.fake_profile_block_video_streaming', + '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': 'Block video streaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_video_streaming', + 'unique_id': 'xyz12_block_video_streaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_video_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block video streaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_video_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_vimeo-entry] + 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.fake_profile_block_vimeo', + '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': 'Block Vimeo', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vimeo', + 'unique_id': 'xyz12_block_vimeo', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_vimeo-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Vimeo', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vimeo', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_vk-entry] + 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.fake_profile_block_vk', + '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': 'Block VK', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vk', + 'unique_id': 'xyz12_block_vk', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_vk-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block VK', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vk', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_whatsapp-entry] + 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.fake_profile_block_whatsapp', + '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': 'Block WhatsApp', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_whatsapp', + 'unique_id': 'xyz12_block_whatsapp', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_whatsapp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block WhatsApp', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_whatsapp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_x_formerly_twitter-entry] + 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.fake_profile_block_x_formerly_twitter', + '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': 'Block X (formerly Twitter)', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitter', + 'unique_id': 'xyz12_block_twitter', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_x_formerly_twitter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block X (formerly Twitter)', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_x_formerly_twitter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_xbox_live-entry] + 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.fake_profile_block_xbox_live', + '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': 'Block Xbox Live', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_xboxlive', + 'unique_id': 'xyz12_block_xboxlive', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_xbox_live-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Xbox Live', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_xbox_live', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_youtube-entry] + 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.fake_profile_block_youtube', + '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': 'Block YouTube', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_youtube', + 'unique_id': 'xyz12_block_youtube', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_youtube-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block YouTube', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_youtube', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_zoom-entry] + 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.fake_profile_block_zoom', + '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': 'Block Zoom', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_zoom', + 'unique_id': 'xyz12_block_zoom', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_zoom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Zoom', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_zoom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_cache_boost-entry] + 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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_cname_flattening-entry] + 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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_cryptojacking_protection-entry] + 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.fake_profile_cryptojacking_protection', + '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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_dns_rebinding_protection-entry] + 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.fake_profile_dns_rebinding_protection', + '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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_domain_generation_algorithms_protection-entry] + 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.fake_profile_domain_generation_algorithms_protection', + '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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_force_safesearch-entry] + 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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_force_youtube_restricted_mode-entry] + 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.fake_profile_force_youtube_restricted_mode', + '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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_google_safe_browsing-entry] + 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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_idn_homograph_attacks_protection-entry] + 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.fake_profile_idn_homograph_attacks_protection', + '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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_logs-entry] + 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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_threat_intelligence_feeds-entry] + 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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_typosquatting_protection-entry] + 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.fake_profile_typosquatting_protection', + '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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_web3-entry] + 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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index 484b4e99aad..19cad755fb4 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -4,42 +4,26 @@ from datetime import timedelta from unittest.mock import patch from nextdns import ApiError +from syrupy import SnapshotAssertion -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import CONNECTION_STATUS, init_integration +from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform -async def test_binary_Sensor(hass: HomeAssistant) -> None: +async def test_binary_sensor( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the binary sensors.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BINARY_SENSOR]): + entry = await init_integration(hass) - await init_integration(hass) - - state = hass.states.get("binary_sensor.fake_profile_device_connection_status") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("binary_sensor.fake_profile_device_connection_status") - assert entry - assert entry.unique_id == "xyz12_this_device_nextdns_connection_status" - - state = hass.states.get( - "binary_sensor.fake_profile_device_profile_connection_status" - ) - assert state - assert state.state == STATE_OFF - - entry = registry.async_get( - "binary_sensor.fake_profile_device_profile_connection_status" - ) - assert entry - assert entry.unique_id == "xyz12_this_device_profile_connection_status" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: @@ -57,19 +41,16 @@ async def test_availability(hass: HomeAssistant) -> None: side_effect=ApiError("API Error"), ): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("binary_sensor.fake_profile_device_connection_status") assert state assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=20) - with patch( - "homeassistant.components.nextdns.NextDns.connection_status", - return_value=CONNECTION_STATUS, - ): + with mock_nextdns(): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("binary_sensor.fake_profile_device_connection_status") assert state diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index b5f7b01aee2..51970b9bb48 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -2,28 +2,27 @@ from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from . import init_integration +from tests.common import snapshot_platform -async def test_button(hass: HomeAssistant) -> None: + +async def test_button( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the button.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BUTTON]): + entry = await init_integration(hass) - await init_integration(hass) - - state = hass.states.get("button.fake_profile_clear_logs") - assert state - assert state.state == STATE_UNKNOWN - - entry = registry.async_get("button.fake_profile_clear_logs") - assert entry - assert entry.unique_id == "xyz12_clear_logs" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_button_press(hass: HomeAssistant) -> None: diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 4d9961474c5..9247288eebf 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError import pytest -from homeassistant import data_entry_flow from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import PROFILES, init_integration @@ -19,7 +19,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,7 +37,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: {CONF_API_KEY: "fake_api_key"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "profiles" result = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fake Profile" assert result["data"][CONF_API_KEY] == "fake_api_key" assert result["data"][CONF_PROFILE_ID] == "xyz12" @@ -97,5 +97,5 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index a6d9b4c545f..e7ea7a3f56b 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -4,283 +4,37 @@ from datetime import timedelta from unittest.mock import patch from nextdns import ApiError +from syrupy import SnapshotAssertion -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integration +from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform async def test_sensor( - hass: HomeAssistant, entity_registry_enabled_by_default: None + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: """Test states of sensors.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - await init_integration(hass) - - state = hass.states.get("sensor.fake_profile_dns_queries") - assert state - assert state.state == "100" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_queries") - assert entry - assert entry.unique_id == "xyz12_all_queries" - - state = hass.states.get("sensor.fake_profile_dns_queries_blocked") - assert state - assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") - assert entry - assert entry.unique_id == "xyz12_blocked_queries" - - state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") - assert state - assert state.state == "20.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_queries_blocked_ratio") - assert entry - assert entry.unique_id == "xyz12_blocked_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_queries_relayed") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") - assert entry - assert entry.unique_id == "xyz12_relayed_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_https_queries") - assert state - assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") - assert entry - assert entry.unique_id == "xyz12_doh_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") - assert state - assert state.state == "17.4" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_https_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_doh_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries") - assert state - assert state.state == "15" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries") - assert entry - assert entry.unique_id == "xyz12_doh3_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries_ratio") - assert state - assert state.state == "13.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_doh3_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") - assert entry - assert entry.unique_id == "xyz12_doq_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") - assert state - assert state.state == "8.7" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_doq_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") - assert state - assert state.state == "30" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") - assert entry - assert entry.unique_id == "xyz12_dot_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") - assert state - assert state.state == "26.1" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_dot_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") - assert state - assert state.state == "25" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") - assert entry - assert entry.unique_id == "xyz12_not_validated_queries" - - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") - assert state - assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") - assert entry - assert entry.unique_id == "xyz12_validated_queries" - - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries_ratio") - assert state - assert state.state == "75.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_validated_queries_ratio" - - state = hass.states.get("sensor.fake_profile_encrypted_queries") - assert state - assert state.state == "60" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_encrypted_queries") - assert entry - assert entry.unique_id == "xyz12_encrypted_queries" - - state = hass.states.get("sensor.fake_profile_unencrypted_queries") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_unencrypted_queries") - assert entry - assert entry.unique_id == "xyz12_unencrypted_queries" - - state = hass.states.get("sensor.fake_profile_encrypted_queries_ratio") - assert state - assert state.state == "60.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_encrypted_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_encrypted_queries_ratio" - - state = hass.states.get("sensor.fake_profile_ipv4_queries") - assert state - assert state.state == "90" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_ipv4_queries") - assert entry - assert entry.unique_id == "xyz12_ipv4_queries" - - state = hass.states.get("sensor.fake_profile_ipv6_queries") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_ipv6_queries") - assert entry - assert entry.unique_id == "xyz12_ipv6_queries" - - state = hass.states.get("sensor.fake_profile_ipv6_queries_ratio") - assert state - assert state.state == "10.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_ipv6_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_ipv6_queries_ratio" - - state = hass.states.get("sensor.fake_profile_tcp_queries") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_tcp_queries") - assert entry - assert entry.unique_id == "xyz12_tcp_queries" - - state = hass.states.get("sensor.fake_profile_tcp_queries_ratio") - assert state - assert state.state == "0.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_tcp_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_tcp_queries_ratio" - - state = hass.states.get("sensor.fake_profile_udp_queries") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_udp_queries") - assert entry - assert entry.unique_id == "xyz12_udp_queries" - - state = hass.states.get("sensor.fake_profile_udp_queries_ratio") - assert state - assert state.state == "34.8" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_udp_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_udp_queries_ratio" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability( - hass: HomeAssistant, entity_registry_enabled_by_default: None + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, ) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" - er.async_get(hass) - await init_integration(hass) state = hass.states.get("sensor.fake_profile_dns_queries") @@ -332,7 +86,7 @@ async def test_availability( ), ): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.fake_profile_dns_queries") assert state @@ -355,30 +109,9 @@ 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 mock_nextdns(): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.fake_profile_dns_queries") assert state diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index f51ee32fd10..2936bad1c67 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -7,6 +7,7 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError import pytest +from syrupy import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -16,614 +17,29 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import SETTINGS, init_integration +from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform async def test_switch( - hass: HomeAssistant, entity_registry_enabled_by_default: None + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: """Test states of the switches.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SWITCH]): + entry = await init_integration(hass) - await init_integration(hass) - - state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_ai_driven_threat_detection") - assert entry - assert entry.unique_id == "xyz12_ai_threat_detection" - - state = hass.states.get("switch.fake_profile_allow_affiliate_tracking_links") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_allow_affiliate_tracking_links") - assert entry - assert entry.unique_id == "xyz12_allow_affiliate" - - state = hass.states.get("switch.fake_profile_anonymized_edns_client_subnet") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_anonymized_edns_client_subnet") - assert entry - assert entry.unique_id == "xyz12_anonymized_ecs" - - state = hass.states.get("switch.fake_profile_block_bypass_methods") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_bypass_methods") - assert entry - assert entry.unique_id == "xyz12_block_bypass_methods" - - state = hass.states.get("switch.fake_profile_block_child_sexual_abuse_material") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_child_sexual_abuse_material") - assert entry - assert entry.unique_id == "xyz12_block_csam" - - state = hass.states.get("switch.fake_profile_block_disguised_third_party_trackers") - assert state - assert state.state == STATE_ON - - entry = registry.async_get( - "switch.fake_profile_block_disguised_third_party_trackers" - ) - assert entry - assert entry.unique_id == "xyz12_block_disguised_trackers" - - state = hass.states.get("switch.fake_profile_block_dynamic_dns_hostnames") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_dynamic_dns_hostnames") - assert entry - assert entry.unique_id == "xyz12_block_ddns" - - state = hass.states.get("switch.fake_profile_block_newly_registered_domains") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_newly_registered_domains") - assert entry - assert entry.unique_id == "xyz12_block_nrd" - - state = hass.states.get("switch.fake_profile_block_page") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_block_page") - assert entry - assert entry.unique_id == "xyz12_block_page" - - state = hass.states.get("switch.fake_profile_block_parked_domains") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_parked_domains") - assert entry - assert entry.unique_id == "xyz12_block_parked_domains" - - state = hass.states.get("switch.fake_profile_cname_flattening") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_cname_flattening") - assert entry - assert entry.unique_id == "xyz12_cname_flattening" - - state = hass.states.get("switch.fake_profile_cache_boost") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_cache_boost") - assert entry - assert entry.unique_id == "xyz12_cache_boost" - - state = hass.states.get("switch.fake_profile_cryptojacking_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_cryptojacking_protection") - assert entry - assert entry.unique_id == "xyz12_cryptojacking_protection" - - state = hass.states.get("switch.fake_profile_dns_rebinding_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_dns_rebinding_protection") - assert entry - assert entry.unique_id == "xyz12_dns_rebinding_protection" - - state = hass.states.get( - "switch.fake_profile_domain_generation_algorithms_protection" - ) - assert state - assert state.state == STATE_ON - - entry = registry.async_get( - "switch.fake_profile_domain_generation_algorithms_protection" - ) - assert entry - assert entry.unique_id == "xyz12_dga_protection" - - state = hass.states.get("switch.fake_profile_force_safesearch") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_force_safesearch") - assert entry - assert entry.unique_id == "xyz12_safesearch" - - state = hass.states.get("switch.fake_profile_force_youtube_restricted_mode") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_force_youtube_restricted_mode") - assert entry - assert entry.unique_id == "xyz12_youtube_restricted_mode" - - state = hass.states.get("switch.fake_profile_google_safe_browsing") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_google_safe_browsing") - assert entry - assert entry.unique_id == "xyz12_google_safe_browsing" - - state = hass.states.get("switch.fake_profile_idn_homograph_attacks_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_idn_homograph_attacks_protection") - assert entry - assert entry.unique_id == "xyz12_idn_homograph_attacks_protection" - - state = hass.states.get("switch.fake_profile_logs") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_logs") - assert entry - assert entry.unique_id == "xyz12_logs" - - state = hass.states.get("switch.fake_profile_threat_intelligence_feeds") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_threat_intelligence_feeds") - assert entry - assert entry.unique_id == "xyz12_threat_intelligence_feeds" - - state = hass.states.get("switch.fake_profile_typosquatting_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_typosquatting_protection") - assert entry - assert entry.unique_id == "xyz12_typosquatting_protection" - - state = hass.states.get("switch.fake_profile_web3") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_web3") - assert entry - assert entry.unique_id == "xyz12_web3" - - state = hass.states.get("switch.fake_profile_block_9gag") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_9gag") - assert entry - assert entry.unique_id == "xyz12_block_9gag" - - state = hass.states.get("switch.fake_profile_block_amazon") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_amazon") - assert entry - assert entry.unique_id == "xyz12_block_amazon" - - state = hass.states.get("switch.fake_profile_block_bereal") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_bereal") - assert entry - assert entry.unique_id == "xyz12_block_bereal" - - state = hass.states.get("switch.fake_profile_block_blizzard") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_blizzard") - assert entry - assert entry.unique_id == "xyz12_block_blizzard" - - state = hass.states.get("switch.fake_profile_block_chatgpt") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_chatgpt") - assert entry - assert entry.unique_id == "xyz12_block_chatgpt" - - state = hass.states.get("switch.fake_profile_block_dailymotion") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_dailymotion") - assert entry - assert entry.unique_id == "xyz12_block_dailymotion" - - state = hass.states.get("switch.fake_profile_block_discord") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_discord") - assert entry - assert entry.unique_id == "xyz12_block_discord" - - state = hass.states.get("switch.fake_profile_block_disney_plus") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_disney_plus") - assert entry - assert entry.unique_id == "xyz12_block_disneyplus" - - state = hass.states.get("switch.fake_profile_block_ebay") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_ebay") - assert entry - assert entry.unique_id == "xyz12_block_ebay" - - state = hass.states.get("switch.fake_profile_block_facebook") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_facebook") - assert entry - assert entry.unique_id == "xyz12_block_facebook" - - state = hass.states.get("switch.fake_profile_block_fortnite") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_fortnite") - assert entry - assert entry.unique_id == "xyz12_block_fortnite" - - state = hass.states.get("switch.fake_profile_block_google_chat") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_google_chat") - assert entry - assert entry.unique_id == "xyz12_block_google_chat" - - state = hass.states.get("switch.fake_profile_block_hbo_max") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_hbo_max") - assert entry - assert entry.unique_id == "xyz12_block_hbomax" - - state = hass.states.get("switch.fake_profile_block_hulu") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_hulu") - assert entry - assert entry.unique_id == "xyz12_block_hulu" - - state = hass.states.get("switch.fake_profile_block_imgur") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_imgur") - assert entry - assert entry.unique_id == "xyz12_block_imgur" - - state = hass.states.get("switch.fake_profile_block_instagram") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_instagram") - assert entry - assert entry.unique_id == "xyz12_block_instagram" - - state = hass.states.get("switch.fake_profile_block_league_of_legends") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_league_of_legends") - assert entry - assert entry.unique_id == "xyz12_block_leagueoflegends" - - state = hass.states.get("switch.fake_profile_block_mastodon") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_mastodon") - assert entry - assert entry.unique_id == "xyz12_block_mastodon" - - state = hass.states.get("switch.fake_profile_block_messenger") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_messenger") - assert entry - assert entry.unique_id == "xyz12_block_messenger" - - state = hass.states.get("switch.fake_profile_block_minecraft") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_minecraft") - assert entry - assert entry.unique_id == "xyz12_block_minecraft" - - state = hass.states.get("switch.fake_profile_block_netflix") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_netflix") - assert entry - assert entry.unique_id == "xyz12_block_netflix" - - state = hass.states.get("switch.fake_profile_block_pinterest") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_pinterest") - assert entry - assert entry.unique_id == "xyz12_block_pinterest" - - state = hass.states.get("switch.fake_profile_block_playstation_network") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_playstation_network") - assert entry - assert entry.unique_id == "xyz12_block_playstation_network" - - state = hass.states.get("switch.fake_profile_block_prime_video") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_prime_video") - assert entry - assert entry.unique_id == "xyz12_block_primevideo" - - state = hass.states.get("switch.fake_profile_block_reddit") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_reddit") - assert entry - assert entry.unique_id == "xyz12_block_reddit" - - state = hass.states.get("switch.fake_profile_block_roblox") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_roblox") - assert entry - assert entry.unique_id == "xyz12_block_roblox" - - state = hass.states.get("switch.fake_profile_block_signal") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_signal") - assert entry - assert entry.unique_id == "xyz12_block_signal" - - state = hass.states.get("switch.fake_profile_block_skype") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_skype") - assert entry - assert entry.unique_id == "xyz12_block_skype" - - state = hass.states.get("switch.fake_profile_block_snapchat") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_snapchat") - assert entry - assert entry.unique_id == "xyz12_block_snapchat" - - state = hass.states.get("switch.fake_profile_block_spotify") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_spotify") - assert entry - assert entry.unique_id == "xyz12_block_spotify" - - state = hass.states.get("switch.fake_profile_block_steam") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_steam") - assert entry - assert entry.unique_id == "xyz12_block_steam" - - state = hass.states.get("switch.fake_profile_block_telegram") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_telegram") - assert entry - assert entry.unique_id == "xyz12_block_telegram" - - state = hass.states.get("switch.fake_profile_block_tiktok") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_tiktok") - assert entry - assert entry.unique_id == "xyz12_block_tiktok" - - state = hass.states.get("switch.fake_profile_block_tinder") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_tinder") - assert entry - assert entry.unique_id == "xyz12_block_tinder" - - state = hass.states.get("switch.fake_profile_block_tumblr") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_tumblr") - assert entry - assert entry.unique_id == "xyz12_block_tumblr" - - state = hass.states.get("switch.fake_profile_block_twitch") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_twitch") - assert entry - assert entry.unique_id == "xyz12_block_twitch" - - state = hass.states.get("switch.fake_profile_block_x_formerly_twitter") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_x_formerly_twitter") - assert entry - assert entry.unique_id == "xyz12_block_twitter" - - state = hass.states.get("switch.fake_profile_block_vimeo") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_vimeo") - assert entry - assert entry.unique_id == "xyz12_block_vimeo" - - state = hass.states.get("switch.fake_profile_block_vk") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_vk") - assert entry - assert entry.unique_id == "xyz12_block_vk" - - state = hass.states.get("switch.fake_profile_block_whatsapp") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_whatsapp") - assert entry - assert entry.unique_id == "xyz12_block_whatsapp" - - state = hass.states.get("switch.fake_profile_block_xbox_live") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_xbox_live") - assert entry - assert entry.unique_id == "xyz12_block_xboxlive" - - state = hass.states.get("switch.fake_profile_block_youtube") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_youtube") - assert entry - assert entry.unique_id == "xyz12_block_youtube" - - state = hass.states.get("switch.fake_profile_block_zoom") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_zoom") - assert entry - assert entry.unique_id == "xyz12_block_zoom" - - state = hass.states.get("switch.fake_profile_block_dating") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_dating") - assert entry - assert entry.unique_id == "xyz12_block_dating" - - state = hass.states.get("switch.fake_profile_block_gambling") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_gambling") - assert entry - assert entry.unique_id == "xyz12_block_gambling" - - state = hass.states.get("switch.fake_profile_block_online_gaming") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_online_gaming") - assert entry - assert entry.unique_id == "xyz12_block_online_gaming" - - state = hass.states.get("switch.fake_profile_block_piracy") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_piracy") - assert entry - assert entry.unique_id == "xyz12_block_piracy" - - state = hass.states.get("switch.fake_profile_block_porn") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_porn") - assert entry - assert entry.unique_id == "xyz12_block_porn" - - state = hass.states.get("switch.fake_profile_block_social_networks") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_social_networks") - assert entry - assert entry.unique_id == "xyz12_block_social_networks" - - state = hass.states.get("switch.fake_profile_block_video_streaming") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_video_streaming") - assert entry - assert entry.unique_id == "xyz12_block_video_streaming" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_switch_on(hass: HomeAssistant) -> None: @@ -693,19 +109,16 @@ async def test_availability(hass: HomeAssistant) -> None: side_effect=ApiError("API Error"), ): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("switch.fake_profile_web3") assert state assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=20) - with patch( - "homeassistant.components.nextdns.NextDns.get_settings", - return_value=SETTINGS, - ): + with mock_nextdns(): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("switch.fake_profile_web3") assert state diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index 04fcf699513..271961fbee7 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import patch from notifications_android_tv.notifications import ConnectError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.nfandroidtv.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_CONFIG_FLOW, @@ -39,7 +40,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -64,7 +65,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +79,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -93,6 +94,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index d7c1fa5ebad..15cd9859d6e 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -50,7 +50,7 @@ class MockConnection(Connection): async def verify_connectivity(self): """Verify that we have functioning communication.""" - def mock_coil_update(self, coil_id: int, value: int | float | str | None): + def mock_coil_update(self, coil_id: int, value: float | str | None): """Trigger an out of band coil update.""" coil = self.heatpump.get_coil_by_address(coil_id) self.coils[coil_id] = value @@ -64,7 +64,7 @@ async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> MockConf entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED return entry diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index b4c0b223998..471f7f4c593 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -43,13 +43,13 @@ async def _get_connection_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": connection_type} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None return result @@ -67,7 +67,7 @@ async def test_nibegw_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "F1155 at 127.0.0.1" assert result2["data"] == { "model": "F1155", @@ -94,7 +94,7 @@ async def test_modbus_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "S1155 at 127.0.0.1" assert result2["data"] == { "model": "S1155", @@ -116,7 +116,7 @@ async def test_modbus_invalid_url( result["flow_id"], {**MOCK_FLOW_MODBUS_USERDATA, "modbus_url": "invalid://url"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"modbus_url": "url"} @@ -131,7 +131,7 @@ async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"listening_port": "address_in_use"} mock_connection.start.side_effect = Exception() @@ -140,7 +140,7 @@ async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -161,7 +161,7 @@ async def test_read_timeout( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "read"} @@ -182,7 +182,7 @@ async def test_write_timeout( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "write"} @@ -203,7 +203,7 @@ async def test_unexpected_exception( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -224,7 +224,7 @@ async def test_nibegw_invalid_host( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM if connection_type == "nibegw": assert result2["errors"] == {"ip_address": "address"} else: @@ -248,5 +248,5 @@ async def test_model_missing_coil( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "model"} diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index c3723596a84..d139a66270c 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.nightscout.const import DOMAIN from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import GLUCOSE_READINGS, SERVER_STATUS, SERVER_STATUS_STATUS_ONLY @@ -24,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -37,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member assert result2["data"] == CONFIG await hass.async_block_till_done() @@ -59,7 +60,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -84,7 +85,7 @@ async def test_user_form_api_key_required(hass: HomeAssistant) -> None: {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -103,7 +104,7 @@ async def test_user_form_unexpected_exception(hass: HomeAssistant) -> None: {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -119,7 +120,7 @@ async def test_user_form_duplicate(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 380d16f5101..7f4f000cf3a 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -65,7 +65,7 @@ async def test_sensors(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.LOADED + assert conf_entry.state is ConfigEntryState.LOADED state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") @@ -181,7 +181,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.LOADED + assert conf_entry.state is ConfigEntryState.LOADED state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") @@ -309,7 +309,7 @@ async def test_sensors_with_area_filter(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.LOADED + assert conf_entry.state is ConfigEntryState.LOADED state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index d3c44258c23..804b614fe92 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -9,7 +9,6 @@ from unittest.mock import patch from pynina import ApiError -from homeassistant import data_entry_flow from homeassistant.components.nina.const import ( CONF_AREA_FILTER, CONF_HEADLINE_FILTER, @@ -25,6 +24,7 @@ from homeassistant.components.nina.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from . import mocked_request_function @@ -61,7 +61,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -75,7 +75,7 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_step_user(hass: HomeAssistant) -> None: @@ -108,7 +108,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "NINA" @@ -122,7 +122,7 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={CONF_HEADLINE_FILTER: ""} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "no_selection"} @@ -141,7 +141,7 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -172,7 +172,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -187,7 +187,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] is None assert dict(config_entry.data) == { @@ -227,7 +227,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,7 +243,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "no_selection"} @@ -272,7 +272,7 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -300,7 +300,7 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: @@ -339,7 +339,7 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entity_registry: er = er.async_get(hass) entries = er.async_entries_for_config_entry( diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index d7c312a8514..5a6b9ab07dd 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -64,7 +64,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: """Test the configuration entry.""" entry: MockConfigEntry = await init_integration(hass) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_sensors_connection_error(hass: HomeAssistant) -> None: @@ -82,4 +82,4 @@ async def test_sensors_connection_error(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.SETUP_RETRY + assert conf_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index ccbdc112e46..2e12c53a759 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, @@ -17,6 +17,7 @@ from homeassistant.components.nmap_tracker.const import ( ) from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import CoreState, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} schema_defaults = result["data_schema"]({}) @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Nmap Tracker {hosts}" assert result2["data"] == {} assert result2["options"] == { @@ -69,7 +70,7 @@ async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -87,7 +88,7 @@ async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Nmap Tracker 192.168.0.5-12" assert result2["data"] == {} assert result2["options"] == { @@ -105,7 +106,7 @@ async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -119,7 +120,7 @@ async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> No ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} @@ -140,7 +141,7 @@ async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -154,7 +155,7 @@ async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -164,7 +165,7 @@ async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -178,7 +179,7 @@ async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} @@ -203,7 +204,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { @@ -232,7 +233,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", CONF_HOME_INTERVAL: 5, diff --git a/tests/components/nobo_hub/test_config_flow.py b/tests/components/nobo_hub/test_config_flow.py index 1d6feb6e28a..61c84f90cd8 100644 --- a/tests/components/nobo_hub/test_config_flow.py +++ b/tests/components/nobo_hub/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import PropertyMock, patch from homeassistant import config_entries from homeassistant.components.nobo_hub.const import CONF_OVERRIDE_TYPE, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -27,7 +28,7 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: "device": "1.1.1.1", }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} assert result2["step_id"] == "selected" @@ -52,7 +53,7 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "My Nobø Ecohub" assert result3["data"] == { "ip_address": "1.1.1.1", @@ -72,7 +73,7 @@ async def test_configure_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -98,7 +99,7 @@ async def test_configure_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Nobø Ecohub" assert result2["data"] == { "serial": "123456789012", @@ -125,7 +126,7 @@ async def test_configure_user_selected_manual(hass: HomeAssistant) -> None: "device": "manual", }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} assert result2["step_id"] == "manual" @@ -151,7 +152,7 @@ async def test_configure_user_selected_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Nobø Ecohub" assert result2["data"] == { "serial": "123456789012", @@ -183,7 +184,7 @@ async def test_configure_invalid_serial_suffix(hass: HomeAssistant) -> None: {"serial_suffix": "ABC"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "invalid_serial"} @@ -202,7 +203,7 @@ async def test_configure_invalid_serial_undiscovered(hass: HomeAssistant) -> Non {"ip_address": "1.1.1.1", "serial": "123456789"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_serial"} @@ -221,7 +222,7 @@ async def test_configure_invalid_ip_address(hass: HomeAssistant) -> None: {"serial": "123456789012", "ip_address": "ABCD"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_ip"} @@ -250,7 +251,7 @@ async def test_configure_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], {"serial_suffix": "012"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012") @@ -271,7 +272,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -281,7 +282,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_OVERRIDE_TYPE: "Constant"} result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -292,5 +293,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_OVERRIDE_TYPE: "Now"} diff --git a/tests/components/notify/conftest.py b/tests/components/notify/conftest.py new file mode 100644 index 00000000000..23930132f7b --- /dev/null +++ b/tests/components/notify/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures for Notify platform tests.""" + +from collections.abc import Generator + +import pytest + +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant + +from tests.common import mock_config_flow, mock_platform + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 0b75a3c4691..1ecfc0d9ecf 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,448 +1,213 @@ -"""The tests for notify services that change targets.""" +"""The tests for notify entity platform.""" -import asyncio -from pathlib import Path -from unittest.mock import Mock, patch +import copy +from unittest.mock import MagicMock import pytest -import yaml +import voluptuous as vol -from homeassistant import config as hass_config from homeassistant.components import notify -from homeassistant.const import SERVICE_RELOAD, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.setup import async_setup_component +from homeassistant.components.notify import ( + DOMAIN, + SERVICE_SEND_MESSAGE, + NotifyEntity, + NotifyEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant, State -from tests.common import MockPlatform, async_get_persistent_notifications, mock_platform +from tests.common import ( + MockConfigEntry, + MockEntity, + MockModule, + mock_integration, + mock_platform, + mock_restore_cache, + setup_test_component_platform, +) + +TEST_KWARGS = {"message": "Test message"} -class MockNotifyPlatform(MockPlatform): - """Help to set up test notify service.""" +class MockNotifyEntity(MockEntity, NotifyEntity): + """Mock Email notitier entity to use in tests.""" - def __init__(self, async_get_service=None, get_service=None): - """Return the notify service.""" - super().__init__() - if get_service: - self.get_service = get_service - if async_get_service: - self.async_get_service = async_get_service + send_message_mock_calls = MagicMock() + + async def async_send_message(self, message: str) -> None: + """Send a notification message.""" + self.send_message_mock_calls(message=message) -def mock_notify_platform( - hass, tmp_path, integration="notify", async_get_service=None, get_service=None -): - """Specialize the mock platform for notify.""" - loaded_platform = MockNotifyPlatform(async_get_service, get_service) - mock_platform(hass, f"{integration}.notify", loaded_platform) +class MockNotifyEntityNonAsync(MockEntity, NotifyEntity): + """Mock Email notitier entity to use in tests.""" - return loaded_platform + send_message_mock_calls = MagicMock() + + def send_message(self, message: str) -> None: + """Send a notification message.""" + self.send_message_mock_calls(message=message) -async def test_same_targets(hass: HomeAssistant) -> None: - """Test not changing the targets in a notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - await test.async_register_services() - await hass.async_block_till_done() - assert test.registered_targets == {"test_a": 1, "test_b": 2} +async def help_async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry +) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + return True -async def test_change_targets(hass: HomeAssistant) -> None: - """Test changing the targets in a notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - test.target_list = {"a": 0} - await test.async_register_services() - await hass.async_block_till_done() - assert test.target_list == {"a": 0} - assert test.registered_targets == {"test_a": 0} +async def help_async_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> bool: + """Unload test config emntry.""" + return await hass.config_entries.async_unload_platforms( + config_entry, [Platform.NOTIFY] + ) -async def test_add_targets(hass: HomeAssistant) -> None: - """Test adding the targets in a notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - test.target_list = {"a": 1, "b": 2, "c": 3} - await test.async_register_services() - await hass.async_block_till_done() - assert test.target_list == {"a": 1, "b": 2, "c": 3} - assert test.registered_targets == {"test_a": 1, "test_b": 2, "test_c": 3} - - -async def test_remove_targets(hass: HomeAssistant) -> None: - """Test removing targets from the targets in a notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - test.target_list = {"c": 1} - await test.async_register_services() - await hass.async_block_till_done() - assert test.target_list == {"c": 1} - assert test.registered_targets == {"test_c": 1} - - -class NotificationService(notify.BaseNotificationService): - """A test class for notification services.""" - - def __init__(self, hass, target_list={"a": 1, "b": 2}, name="notify"): - """Initialize the service.""" - - async def _async_make_reloadable(hass): - """Initialize the reload service.""" - await async_setup_reload_service(hass, name, [notify.DOMAIN]) - - self.hass = hass - self.target_list = target_list - hass.async_create_task(_async_make_reloadable(hass)) - - @property - def targets(self): - """Return a dictionary of devices.""" - return self.target_list - - -async def test_warn_template( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture +@pytest.mark.parametrize( + "entity", + [ + MockNotifyEntityNonAsync(name="test", entity_id="notify.test"), + MockNotifyEntity(name="test", entity_id="notify.test"), + ], + ids=["non_async", "async"], +) +async def test_send_message_service( + hass: HomeAssistant, config_flow_fixture: None, entity: NotifyEntity ) -> None: - """Test warning when template used.""" - assert await async_setup_component(hass, "notify", {}) + """Test send_message service.""" + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + ) + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get("notify.test") + assert state.state is STATE_UNKNOWN await hass.services.async_call( - "notify", - "persistent_notification", - {"message": "{{ 1 + 1 }}", "title": "Test notif {{ 1 + 1 }}"}, + DOMAIN, + SERVICE_SEND_MESSAGE, + copy.deepcopy(TEST_KWARGS) | {"entity_id": "notify.test"}, blocking=True, ) - # We should only log it once - assert caplog.text.count("Passing templates to notify service is deprecated") == 1 - notifications = async_get_persistent_notifications(hass) - assert len(notifications) == 1 - - -async def test_invalid_platform( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup with an invalid platform.""" - mock_notify_platform(hass, tmp_path, "testnotify1") - # Setup the platform - await async_setup_component( - hass, "notify", {"notify": [{"platform": "testnotify1"}]} - ) await hass.async_block_till_done() - assert "Invalid notify platform" in caplog.text - caplog.clear() - # Setup the second testnotify2 platform dynamically - mock_notify_platform(hass, tmp_path, "testnotify2") - await async_load_platform( - hass, - "notify", - "testnotify2", - {}, - hass_config={"notify": [{"platform": "testnotify2"}]}, - ) - await hass.async_block_till_done() - assert "Invalid notify platform" in caplog.text + entity.send_message_mock_calls.assert_called_once() + entity.send_message_mock_calls.reset_mock() -async def test_invalid_service( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup with an invalid service object or platform.""" - - def get_service(hass, config, discovery_info=None): - """Return None for an invalid notify service.""" - return None - - mock_notify_platform(hass, tmp_path, "testnotify", get_service=get_service) - # Setup the second testnotify2 platform dynamically - await async_load_platform( - hass, - "notify", - "testnotify", - {}, - hass_config={"notify": [{"platform": "testnotify"}]}, - ) - await hass.async_block_till_done() - assert "Failed to initialize notification service testnotify" in caplog.text - caplog.clear() - - await async_load_platform( - hass, - "notify", - "testnotifyinvalid", - {"notify": [{"platform": "testnotifyinvalid"}]}, - hass_config={"notify": [{"platform": "testnotifyinvalid"}]}, - ) - await hass.async_block_till_done() - assert "Unknown notification service specified" in caplog.text - - -async def test_platform_setup_with_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup with an invalid setup.""" - - async def async_get_service(hass, config, discovery_info=None): - """Return None for an invalid notify service.""" - raise Exception("Setup error") - - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - # Setup the second testnotify2 platform dynamically - await async_load_platform( - hass, - "notify", - "testnotify", - {}, - hass_config={"notify": [{"platform": "testnotify"}]}, - ) - await hass.async_block_till_done() - assert "Error setting up platform testnotify" in caplog.text - - -async def test_reload_with_notify_builtin_platform_reload( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test reload using the notify platform reload method.""" - - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - # platform with service - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Perform a reload using the notify module for testnotify (without services) - await notify.async_reload(hass, "testnotify") - - # Setup the platform - await async_setup_component( - hass, "notify", {"notify": [{"platform": "testnotify"}]} - ) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - - # Perform a reload using the notify module for testnotify (with services) - await notify.async_reload(hass, "testnotify") - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - - -async def test_setup_platform_and_reload( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup and reload.""" - get_service_called = Mock() - - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - async def async_get_service2(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"c": 3, "d": 4} - return NotificationService(hass, targetlist, "testnotify2") - - # Mock first platform - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Initialize a second platform testnotify2 - mock_notify_platform( - hass, tmp_path, "testnotify2", async_get_service=async_get_service2 - ) - - # Setup the testnotify platform - await async_setup_component( - hass, "notify", {"notify": [{"platform": "testnotify"}]} - ) - await hass.async_block_till_done() - assert hass.services.has_service("testnotify", SERVICE_RELOAD) - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert get_service_called.call_count == 1 - assert get_service_called.call_args[0][0] == {"platform": "testnotify"} - assert get_service_called.call_args[0][1] is None - get_service_called.reset_mock() - - # Setup the second testnotify2 platform dynamically - await async_load_platform( - hass, - "notify", - "testnotify2", - {}, - hass_config={"notify": [{"platform": "testnotify"}]}, - ) - await hass.async_block_till_done() - assert hass.services.has_service("testnotify2", SERVICE_RELOAD) - assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") - assert get_service_called.call_count == 1 - assert get_service_called.call_args[0][0] == {} - assert get_service_called.call_args[0][1] == {} - get_service_called.reset_mock() - - # Perform a reload - new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({"notify": [{"platform": "testnotify"}]}) - new_yaml_config_file.write_text(new_yaml_config) - - with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + # Test schema: `None` message fails + with pytest.raises(vol.Invalid) as exc: await hass.services.async_call( - "testnotify", - SERVICE_RELOAD, - {}, - blocking=True, + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + {"entity_id": "notify.test", notify.ATTR_MESSAGE: None}, ) + assert ( + str(exc.value) == "string value is None for dictionary value @ data['message']" + ) + entity.send_message_mock_calls.assert_not_called() + + # Test schema: No message fails + with pytest.raises(vol.Invalid) as exc: await hass.services.async_call( - "testnotify2", - SERVICE_RELOAD, - {}, - blocking=True, + notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, {"entity_id": "notify.test"} ) - await hass.async_block_till_done() + assert str(exc.value) == "required key not provided @ data['message']" + entity.send_message_mock_calls.assert_not_called() - # Check if the notify services from setup still exist - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert get_service_called.call_count == 1 - assert get_service_called.call_args[0][0] == {"platform": "testnotify"} - assert get_service_called.call_args[0][1] is None - - # Check if the dynamically notify services from setup were removed - assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") + # Test unloading the entry succeeds + assert await hass.config_entries.async_unload(config_entry.entry_id) -async def test_setup_platform_before_notify_setup( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +@pytest.mark.parametrize( + ("state", "init_state"), + [ + ("2021-01-01T23:59:59+00:00", "2021-01-01T23:59:59+00:00"), + (STATE_UNAVAILABLE, STATE_UNKNOWN), + ], +) +async def test_restore_state( + hass: HomeAssistant, config_flow_fixture: None, state: str, init_state: str ) -> None: - """Test trying to setup a platform before notify is setup.""" - get_service_called = Mock() + """Test we restore state integration.""" + mock_restore_cache(hass, (State("notify.test", state),)) - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - async def async_get_service2(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"c": 3, "d": 4} - return NotificationService(hass, targetlist, "testnotify2") - - # Mock first platform - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + ), ) - # Initialize a second platform testnotify2 - mock_notify_platform( - hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + entity = MockNotifyEntity(name="test", entity_id="notify.test") + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get("notify.test") + assert state is not None + assert state.state is init_state + + +async def test_name(hass: HomeAssistant, config_flow_fixture: None) -> None: + """Test notify name.""" + + mock_platform(hass, "test.config_flow") + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + ), ) - hass_config = {"notify": [{"platform": "testnotify"}]} + # Unnamed notify entity -> no name + entity1 = NotifyEntity() + entity1.entity_id = "notify.test1" - # Setup the second testnotify2 platform from discovery - load_coro = async_load_platform( - hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + # Unnamed notify entity and has_entity_name True -> unnamed + entity2 = NotifyEntity() + entity2.entity_id = "notify.test3" + entity2._attr_has_entity_name = True + + # Named notify entity and has_entity_name True -> named + entity3 = NotifyEntity() + entity3.entity_id = "notify.test4" + entity3.entity_description = NotifyEntityDescription("test", has_entity_name=True) + + setup_test_component_platform( + hass, DOMAIN, [entity1, entity2, entity3], from_config_entry=True ) - # Setup the testnotify platform - setup_coro = async_setup_component(hass, "notify", hass_config) - - load_task = asyncio.create_task(load_coro) - setup_task = asyncio.create_task(setup_coro) - - await asyncio.gather(load_task, setup_task) - + 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 hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + state = hass.states.get(entity1.entity_id) + assert state + assert state.attributes == {} -async def test_setup_platform_after_notify_setup( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test trying to setup a platform after notify is setup.""" - get_service_called = Mock() + state = hass.states.get(entity2.entity_id) + assert state + assert state.attributes == {} - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - async def async_get_service2(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"c": 3, "d": 4} - return NotificationService(hass, targetlist, "testnotify2") - - # Mock first platform - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Initialize a second platform testnotify2 - mock_notify_platform( - hass, tmp_path, "testnotify2", async_get_service=async_get_service2 - ) - - hass_config = {"notify": [{"platform": "testnotify"}]} - - # Setup the second testnotify2 platform from discovery - load_coro = async_load_platform( - hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config - ) - - # Setup the testnotify platform - setup_coro = async_setup_component(hass, "notify", hass_config) - - setup_task = asyncio.create_task(setup_coro) - load_task = asyncio.create_task(load_coro) - - await asyncio.gather(load_task, setup_task) - - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + state = hass.states.get(entity3.entity_id) + assert state + assert state.attributes == {} diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py new file mode 100644 index 00000000000..71424beeda9 --- /dev/null +++ b/tests/components/notify/test_legacy.py @@ -0,0 +1,625 @@ +"""The tests for legacy notify services.""" + +import asyncio +from collections.abc import Mapping +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +import pytest +import voluptuous as vol +import yaml + +from homeassistant import config as hass_config +from homeassistant.components import notify +from homeassistant.const import SERVICE_RELOAD, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.setup import async_setup_component + +from tests.common import MockPlatform, async_get_persistent_notifications, mock_platform + + +class NotificationService(notify.BaseNotificationService): + """A test class for legacy notification services.""" + + def __init__( + self, + hass: HomeAssistant, + target_list: dict[str, Any] | None = None, + name="notify", + ) -> None: + """Initialize the service.""" + + async def _async_make_reloadable(hass: HomeAssistant) -> None: + """Initialize the reload service.""" + await async_setup_reload_service(hass, name, [notify.DOMAIN]) + + self.hass = hass + self.target_list = target_list or {"a": 1, "b": 2} + hass.async_create_task(_async_make_reloadable(hass)) + + @property + def targets(self): + """Return a dictionary of devices.""" + return self.target_list + + +class MockNotifyPlatform(MockPlatform): + """Help to set up a legacy test notify service.""" + + def __init__(self, async_get_service: Any = None, get_service: Any = None) -> None: + """Return a legacy notify service.""" + super().__init__() + if get_service: + self.get_service = get_service + if async_get_service: + self.async_get_service = async_get_service + + +def mock_notify_platform( + hass: HomeAssistant, + tmp_path: Path, + integration: str = "notify", + async_get_service: Any = None, + get_service: Any = None, +): + """Specialize the mock platform for legacy notify service.""" + loaded_platform = MockNotifyPlatform(async_get_service, get_service) + mock_platform(hass, f"{integration}.notify", loaded_platform) + + return loaded_platform + + +async def help_setup_notify( + hass: HomeAssistant, tmp_path: Path, targets: dict[str, None] | None = None +) -> MagicMock: + """Help set up a platform notify service.""" + send_message_mock = MagicMock() + + class _TestNotifyService(notify.BaseNotificationService): + def __init__(self, targets: dict[str, None] | None) -> None: + """Initialize service.""" + self._targets = targets + super().__init__() + + @property + def targets(self) -> Mapping[str, Any] | None: + """Return a dictionary of registered targets.""" + return self._targets + + def send_message(self, message: str, **kwargs: Any) -> None: + """Send a message.""" + send_message_mock(message, kwargs) + + async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> notify.BaseNotificationService: + """Get notify service for mocked platform.""" + return _TestNotifyService(targets) + + # Mock platform with service + mock_notify_platform(hass, tmp_path, "test", async_get_service=async_get_service) + # Setup the platform + await async_setup_component(hass, "notify", {"notify": [{"platform": "test"}]}) + await hass.async_block_till_done() + + # Return mock for assertion service calls + return send_message_mock + + +async def test_same_targets(hass: HomeAssistant) -> None: + """Test not changing the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + await test.async_register_services() + await hass.async_block_till_done() + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + +async def test_change_targets(hass: HomeAssistant) -> None: + """Test changing the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"a": 0} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"a": 0} + assert test.registered_targets == {"test_a": 0} + + +async def test_add_targets(hass: HomeAssistant) -> None: + """Test adding the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"a": 1, "b": 2, "c": 3} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"a": 1, "b": 2, "c": 3} + assert test.registered_targets == {"test_a": 1, "test_b": 2, "test_c": 3} + + +async def test_remove_targets(hass: HomeAssistant) -> None: + """Test removing targets from the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"c": 1} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"c": 1} + assert test.registered_targets == {"test_c": 1} + + +async def test_warn_template( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test warning when template used.""" + assert await async_setup_component(hass, "notify", {}) + + await hass.services.async_call( + "notify", + "persistent_notification", + {"message": "{{ 1 + 1 }}", "title": "Test notif {{ 1 + 1 }}"}, + blocking=True, + ) + # We should only log it once + assert caplog.text.count("Passing templates to notify service is deprecated") == 1 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + + +async def test_invalid_platform( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup with an invalid platform.""" + mock_notify_platform(hass, tmp_path, "testnotify1") + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify1"}]} + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + caplog.clear() + # Setup the second testnotify2 platform dynamically + mock_notify_platform(hass, tmp_path, "testnotify2") + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify2"}]}, + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + + +async def test_invalid_service( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup with an invalid service object or platform.""" + + def get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + return None + + mock_notify_platform(hass, tmp_path, "testnotify", get_service=get_service) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Failed to initialize notification service testnotify" in caplog.text + caplog.clear() + + await async_load_platform( + hass, + "notify", + "testnotifyinvalid", + {"notify": [{"platform": "testnotifyinvalid"}]}, + hass_config={"notify": [{"platform": "testnotifyinvalid"}]}, + ) + await hass.async_block_till_done() + assert "Unknown notification service specified" in caplog.text + + +async def test_platform_setup_with_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup with an invalid setup.""" + + async def async_get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + raise Exception("Setup error") + + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Error setting up platform testnotify" in caplog.text + + +async def test_reload_with_notify_builtin_platform_reload( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test reload using the legacy notify platform reload method.""" + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + # platform with service + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Perform a reload using the notify module for testnotify (without services) + await notify.async_reload(hass, "testnotify") + + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + # Perform a reload using the notify module for testnotify (with services) + await notify.async_reload(hass, "testnotify") + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + +async def test_setup_platform_and_reload( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup and reload.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get legacy notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + # Setup the testnotify platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + get_service_called.reset_mock() + + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify2", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {} + assert get_service_called.call_args[0][1] == {} + get_service_called.reset_mock() + + # Perform a reload + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({"notify": [{"platform": "testnotify"}]}) + new_yaml_config_file.write_text(new_yaml_config) + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await hass.services.async_call( + "testnotify", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.services.async_call( + "testnotify2", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + # Check if the notify services from setup still exist + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + + # Check if the dynamically notify services from setup were removed + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_before_notify_setup( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test trying to setup a platform before legacy notify service is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + load_task = asyncio.create_task(load_coro) + setup_task = asyncio.create_task(setup_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_after_notify_setup( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test trying to setup a platform after legacy notify service is set up.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + setup_task = asyncio.create_task(setup_coro) + load_task = asyncio.create_task(load_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_sending_none_message(hass: HomeAssistant, tmp_path: Path) -> None: + """Test send with None as message.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + with pytest.raises(vol.Invalid) as exc: + await hass.services.async_call( + notify.DOMAIN, notify.SERVICE_NOTIFY, {notify.ATTR_MESSAGE: None} + ) + await hass.async_block_till_done() + assert ( + str(exc.value) + == "template value is None for dictionary value @ data['message']" + ) + send_message_mock.assert_not_called() + + +async def test_sending_templated_message(hass: HomeAssistant, tmp_path: Path) -> None: + """Send a templated message.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + hass.states.async_set("sensor.temperature", 10) + data = { + notify.ATTR_MESSAGE: "{{states.sensor.temperature.state}}", + notify.ATTR_TITLE: "{{ states.sensor.temperature.name }}", + } + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "10", {"title": "temperature", "data": None} + ) + + +async def test_method_forwards_correct_data( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test that all data from the service gets forwarded to service.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + data = { + notify.ATTR_MESSAGE: "my message", + notify.ATTR_TITLE: "my title", + notify.ATTR_DATA: {"hello": "world"}, + } + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "my message", {"title": "my title", "data": {"hello": "world"}} + ) + + +async def test_calling_notify_from_script_loaded_from_yaml_without_title( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test if we can call a notify from a script.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + step = { + "service": "notify.notify", + "data": { + "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} + }, + "data_template": {"message": "Test 123 {{ 2 + 2 }}\n"}, + } + await async_setup_component( + hass, "script", {"script": {"test": {"sequence": step}}} + ) + await hass.services.async_call("script", "test") + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "Test 123 4", + {"data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}}}, + ) + + +async def test_calling_notify_from_script_loaded_from_yaml_with_title( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test if we can call a notify from a script.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + step = { + "service": "notify.notify", + "data": { + "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} + }, + "data_template": {"message": "Test 123 {{ 2 + 2 }}\n", "title": "Test"}, + } + await async_setup_component( + hass, "script", {"script": {"test": {"sequence": step}}} + ) + await hass.services.async_call("script", "test") + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "Test 123 4", + { + "title": "Test", + "data": { + "push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"} + }, + }, + ) + + +async def test_targets_are_services(hass: HomeAssistant, tmp_path: Path) -> None: + """Test that all targets are exposed as individual services.""" + await help_setup_notify(hass, tmp_path, targets={"a": 1, "b": 2}) + assert hass.services.has_service("notify", "notify") is not None + assert hass.services.has_service("notify", "test_a") is not None + assert hass.services.has_service("notify", "test_b") is not None + + +async def test_messages_to_targets_route(hass: HomeAssistant, tmp_path: Path) -> None: + """Test message routing to specific target services.""" + send_message_mock = await help_setup_notify( + hass, tmp_path, targets={"target_name": "test target id"} + ) + + await hass.services.async_call( + "notify", + "test_target_name", + {"message": "my message", "title": "my title", "data": {"hello": "world"}}, + ) + await hass.async_block_till_done() + + send_message_mock.assert_called_once_with( + "my message", + {"target": ["test target id"], "title": "my title", "data": {"hello": "world"}}, + ) diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 827565db339..2cc5e3f04b7 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch from aionotion.errors import InvalidCredentialsError, NotionError import pytest -from homeassistant import data_entry_flow from homeassistant.components.notion import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_PASSWORD, TEST_REFRESH_TOKEN, TEST_USER_UUID, TEST_USERNAME @@ -35,7 +35,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when getting a Notion API client: @@ -51,7 +51,7 @@ async def test_create_entry( CONF_PASSWORD: TEST_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors result = await hass.config_entries.flow.async_configure( @@ -61,7 +61,7 @@ async def test_create_entry( CONF_PASSWORD: TEST_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_REFRESH_TOKEN: TEST_REFRESH_TOKEN, @@ -75,7 +75,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,7 +115,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors result = await hass.config_entries.flow.async_configure( @@ -126,6 +126,6 @@ async def test_reauth( # to setup the config entry via reload. await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 1e7a6215143..f96edf82c0b 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.nuheat.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .mocks import _get_mock_thermostat_run @@ -19,7 +20,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_thermostat = _get_mock_thermostat_run() @@ -47,7 +48,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Master bathroom" assert result2["data"] == { CONF_SERIAL_NUMBER: "12345", @@ -76,7 +77,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} response_mock = MagicMock() @@ -94,7 +95,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -126,7 +127,7 @@ async def test_form_invalid_thermostat(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_thermostat"} @@ -149,5 +150,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index c7575f71545..58cbfde3d92 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import patch from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.nuki.const import DOMAIN from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .mock import DHCP_FORMATTED_MAC, HOST, MOCK_INFO, NAME, setup_nuki_integration @@ -20,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -72,7 +73,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,7 +96,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -118,7 +119,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -142,7 +143,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -156,7 +157,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -178,7 +179,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -201,7 +202,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -212,7 +213,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -231,7 +232,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_TOKEN] == "new-token" @@ -243,7 +244,7 @@ async def test_reauth_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -255,7 +256,7 @@ async def test_reauth_invalid_auth(hass: HomeAssistant) -> None: user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} @@ -267,7 +268,7 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -279,7 +280,7 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "cannot_connect"} @@ -291,7 +292,7 @@ async def test_reauth_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -303,6 +304,6 @@ async def test_reauth_unknown_exception(hass: HomeAssistant) -> None: user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index 0c34f1bf53c..92a7cefd467 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.number import DOMAIN, device_action from homeassistant.const import EntityCategory diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 07d2baf4926..96ad4b4d2d4 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -36,6 +36,7 @@ from homeassistant.const import ( UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY @@ -359,14 +360,20 @@ async def test_set_value( state = hass.states.get("number.test") assert state.state == "60.0" - # test ValueError trigger - with pytest.raises(ValueError): + # test range validation + with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"}, blocking=True, ) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "out_of_range" + assert ( + str(exc.value) + == "Value 110.0 for number.test is outside valid range 0.0 - 100.0" + ) await hass.async_block_till_done() state = hass.states.get("number.test") diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 56a7d7d9089..537b6aba5ac 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aionut import NUTError, NUTLoginError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components import zeroconf from homeassistant.components.nut.const import DOMAIN from homeassistant.const import ( @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import _get_mock_nutclient @@ -47,7 +48,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -71,7 +72,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "192.168.1.5:1234" assert result2["data"] == { CONF_HOST: "192.168.1.5", @@ -89,7 +90,7 @@ async def test_form_user_one_ups(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_nutclient( @@ -117,7 +118,7 @@ async def test_form_user_one_ups(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -141,7 +142,7 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_nutclient( @@ -164,7 +165,7 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with ( patch( @@ -182,7 +183,7 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ups2@1.1.1.1:2222" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -205,7 +206,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass: HomeAssistant) -> None 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["type"] is FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_nutclient( @@ -233,7 +234,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -266,7 +267,7 @@ async def test_form_no_upses_found(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_ups_found" @@ -296,7 +297,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert result2["description_placeholders"] == {"error": "no route to host"} @@ -320,7 +321,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} mock_pynut = _get_mock_nutclient( @@ -347,7 +348,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -384,7 +385,7 @@ async def test_auth_failures(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} mock_pynut = _get_mock_nutclient( @@ -411,7 +412,7 @@ async def test_auth_failures(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -457,7 +458,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} mock_pynut = _get_mock_nutclient( @@ -482,7 +483,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -520,7 +521,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -559,7 +560,7 @@ async def test_abort_if_already_setup_alias(hass: HomeAssistant) -> None: ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.nut.AIONUTClient", @@ -570,7 +571,7 @@ async def test_abort_if_already_setup_alias(hass: HomeAssistant) -> None: {CONF_ALIAS: "ups1"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -587,14 +588,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 60, } @@ -602,7 +603,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -610,7 +611,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_SCAN_INTERVAL: 12}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 12, } diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 3bc48764816..b6c9cffd390 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -33,14 +33,14 @@ def _get_mock_nutclient( async def async_init_integration( hass: HomeAssistant, - ups_fixture: str = None, + ups_fixture: str | None = None, username: str = "mock", password: str = "mock", - list_ups: dict[str, str] = None, - list_vars: dict[str, str] = None, - list_commands_return_value: dict[str, str] = None, + list_ups: dict[str, str] | None = None, + list_vars: dict[str, str] | None = None, + list_commands_return_value: dict[str, str] | None = None, list_commands_side_effect=None, - run_command: MagicMock = None, + run_command: MagicMock | None = None, ) -> MockConfigEntry: """Set up the nut integration in Home Assistant.""" diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 7ffde0c5731..ac2c281c57b 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -1,5 +1,6 @@ """Fixtures for National Weather Service tests.""" +import asyncio from unittest.mock import AsyncMock, patch import pytest @@ -24,6 +25,23 @@ def mock_simple_nws(): yield mock_nws +@pytest.fixture +def mock_simple_nws_times_out(): + """Mock pynws SimpleNWS that times out.""" + with patch("homeassistant.components.nws.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.set_station = AsyncMock(side_effect=asyncio.TimeoutError) + instance.update_observation = AsyncMock(side_effect=asyncio.TimeoutError) + instance.update_forecast = AsyncMock(side_effect=asyncio.TimeoutError) + instance.update_forecast_hourly = AsyncMock(side_effect=asyncio.TimeoutError) + instance.station = "ABC" + instance.stations = ["ABC"] + instance.observation = None + instance.forecast = None + instance.forecast_hourly = None + yield mock_nws + + @pytest.fixture def mock_simple_nws_config(): """Mock pynws SimpleNWS with default values in config_flow.""" diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index fe8017c55e1..b20f038c9f7 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -7,6 +7,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.nws.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant, mock_simple_nws_config) -> None: @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_simple_nws_config) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant, mock_simple_nws_config) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ABC" assert result2["data"] == { "api_key": "test", @@ -54,7 +55,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, mock_simple_nws_config) {"api_key": "test"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -72,7 +73,7 @@ async def test_form_unknown_error(hass: HomeAssistant, mock_simple_nws_config) - {"api_key": "test"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -94,7 +95,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 result = await hass.config_entries.flow.async_init( @@ -111,6 +112,6 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 0fb5654d7ee..ad40b576a8a 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -476,3 +476,49 @@ async def test_forecast_subscription( assert forecast2 != [] assert forecast2 == snapshot + + +@pytest.mark.parametrize( + ("forecast_type", "entity_id"), + [("hourly", "weather.abc")], +) +async def test_forecast_subscription_with_failing_coordinator( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + mock_simple_nws_times_out, + no_sensor, + forecast_type: str, + entity_id: str, +) -> None: + """Test a forecast subscription when the coordinator is failing to update.""" + client = await hass_ws_client(hass) + + registry = er.async_get(hass) + # Pre-create the hourly entity + registry.async_get_or_create( + WEATHER_DOMAIN, + nws.DOMAIN, + "35_-75_hourly", + suggested_object_id="abc_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() + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": entity_id, + } + ) + msg = await client.receive_json() + assert not msg["success"] diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index c299d1d6dd5..fb826407558 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +43,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} @@ -56,7 +56,7 @@ async def test_user_form_show_advanced_options(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} user_input_advanced = { @@ -76,7 +76,7 @@ async def test_user_form_show_advanced_options(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} @@ -98,7 +98,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_user_form_unexpected_exception(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -131,5 +131,5 @@ async def test_user_form_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/obihai/test_config_flow.py b/tests/components/obihai/test_config_flow.py index d9ca52424c2..4ad06f33cd1 100644 --- a/tests/components/obihai/test_config_flow.py +++ b/tests/components/obihai/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -35,7 +35,7 @@ async def test_user_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT} @@ -55,7 +55,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -73,7 +73,7 @@ async def test_connect_failure(hass: HomeAssistant, mock_gaierror: Generator) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -92,7 +92,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: ) flows = hass.config_entries.flow.async_progress() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(flows) == 1 assert ( get_schema_suggestion(result["data_schema"].schema, CONF_USERNAME) @@ -114,7 +114,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_dhcp_flow_auth_failure(hass: HomeAssistant) -> None: diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py index 4a896736329..0a35d0a2267 100644 --- a/tests/components/octoprint/__init__.py +++ b/tests/components/octoprint/__init__.py @@ -85,4 +85,4 @@ async def init_integration( 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 is ConfigEntryState.LOADED diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 4c8e22e524c..738fbea0887 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch from pyoctoprintapi import ApiError, DiscoverySettings -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.components.octoprint.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -36,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -60,7 +61,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "username": "testuser", @@ -93,7 +94,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -102,7 +103,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.get_discovery_info", @@ -122,7 +123,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" @@ -143,7 +144,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -152,7 +153,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: result["flow_id"], ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.get_discovery_info", @@ -172,7 +173,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "unknown" @@ -192,7 +193,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] result = await hass.config_entries.flow.async_configure( @@ -201,7 +202,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: "username": "testuser", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -211,7 +212,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -242,7 +243,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_show_ssdp_form(hass: HomeAssistant) -> None: @@ -261,7 +262,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] result = await hass.config_entries.flow.async_configure( @@ -270,7 +271,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: "username": "testuser", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -280,7 +281,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -311,7 +312,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_import_yaml(hass: HomeAssistant) -> None: @@ -347,7 +348,7 @@ async def test_import_yaml(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "errors" not in result @@ -384,7 +385,7 @@ async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(request_app_key.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -405,7 +406,7 @@ async def test_failed_auth(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch("pyoctoprintapi.OctoprintClient.request_app_key", side_effect=ApiError): result = await hass.config_entries.flow.async_configure( @@ -413,10 +414,10 @@ async def test_failed_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_failed" @@ -437,7 +438,7 @@ async def test_failed_auth_unexpected_error(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch("pyoctoprintapi.OctoprintClient.request_app_key", side_effect=Exception): result = await hass.config_entries.flow.async_configure( @@ -445,10 +446,10 @@ async def test_failed_auth_unexpected_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_failed" @@ -464,7 +465,7 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -482,7 +483,7 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -506,7 +507,7 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -534,7 +535,7 @@ async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -560,7 +561,7 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -588,7 +589,7 @@ async def test_reauth_form(hass: HomeAssistant) -> None: }, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -601,7 +602,7 @@ async def test_reauth_form(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "homeassistant.components.octoprint.async_setup_entry", @@ -612,5 +613,5 @@ async def test_reauth_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py index 78ecf0766d7..db1689bd416 100644 --- a/tests/components/ollama/conftest.py +++ b/tests/components/ollama/conftest.py @@ -35,3 +35,9 @@ async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfig ): assert await async_setup_component(hass, ollama.DOMAIN, {}) await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/ollama/test_config_flow.py b/tests/components/ollama/test_config_flow.py index 825f3eac436..b1b74197139 100644 --- a/tests/components/ollama/test_config_flow.py +++ b/tests/components/ollama/test_config_flow.py @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -49,13 +49,13 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Step 2: model - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is 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["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { ollama.CONF_URL: "http://localhost:11434", ollama.CONF_MODEL: TEST_MODEL, @@ -75,7 +75,7 @@ async def test_form_need_download(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None pull_ready = asyncio.Event() @@ -113,14 +113,14 @@ async def test_form_need_download(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Step 2: model - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is 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 + assert result3["type"] is FlowResultType.SHOW_PROGRESS result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], ) @@ -128,12 +128,12 @@ async def test_form_need_download(hass: HomeAssistant) -> None: # Run again without the task finishing. # We should still be downloading. - assert result4["type"] == FlowResultType.SHOW_PROGRESS + assert result4["type"] is 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 + assert result4["type"] is FlowResultType.SHOW_PROGRESS # Signal fake pull method to complete pull_ready.set() @@ -147,7 +147,7 @@ async def test_form_need_download(hass: HomeAssistant) -> None: result4["flow_id"], ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["data"] == { ollama.CONF_URL: "http://localhost:11434", ollama.CONF_MODEL: TEST_MODEL, @@ -167,7 +167,7 @@ async def test_options( {ollama.CONF_PROMPT: "test prompt", ollama.CONF_MAX_HISTORY: 100}, ) await hass.async_block_till_done() - assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"] == { ollama.CONF_PROMPT: "test prompt", ollama.CONF_MAX_HISTORY: 100, @@ -195,7 +195,7 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None: result["flow_id"], {ollama.CONF_URL: "http://localhost:11434"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -205,6 +205,10 @@ async def test_download_error(hass: HomeAssistant) -> None: ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} ) + async def _delayed_runtime_error(*args, **kwargs): + await asyncio.sleep(0) + raise RuntimeError + with ( patch( "homeassistant.components.ollama.config_flow.ollama.AsyncClient.list", @@ -212,7 +216,7 @@ async def test_download_error(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.ollama.config_flow.ollama.AsyncClient.pull", - side_effect=RuntimeError(), + _delayed_runtime_error, ), ): result2 = await hass.config_entries.flow.async_configure( @@ -220,15 +224,15 @@ async def test_download_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is 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 + assert result3["type"] is 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["type"] is FlowResultType.ABORT assert result4["reason"] == "download_failed" diff --git a/tests/components/ollama/test_init.py b/tests/components/ollama/test_init.py index ffe69ca4628..5326a8ed609 100644 --- a/tests/components/ollama/test_init.py +++ b/tests/components/ollama/test_init.py @@ -229,7 +229,7 @@ async def test_message_history_pruning( assert isinstance(result.conversation_id, str) conversation_ids.append(result.conversation_id) - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert isinstance(agent, ollama.OllamaAgent) @@ -284,7 +284,7 @@ async def test_message_history_unlimited( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), result - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert isinstance(agent, ollama.OllamaAgent) @@ -340,7 +340,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test OllamaAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == MATCH_ALL diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index 1c0d6aefcf8..b4955220916 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import patch from omnilogic import LoginException, OmniLogicException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.omnilogic.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -38,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Omnilogic" assert result2["data"] == DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -52,7 +53,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -72,7 +73,7 @@ async def test_with_invalid_credentials(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -93,7 +94,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -114,7 +115,7 @@ async def test_with_unknown_error(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -134,7 +135,7 @@ async def test_option_flow(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -142,6 +143,6 @@ async def test_option_flow(hass: HomeAssistant) -> None: user_input={"polling_interval": 9}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["polling_interval"] == 9 diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index df1452b176e..d88774307c0 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from unittest.mock import patch -from aiooncue import OncueDevice, OncueSensor +from aiooncue import LoginFailedException, OncueDevice, OncueSensor MOCK_ASYNC_FETCH_ALL = { "123456": OncueDevice( @@ -861,3 +861,21 @@ def _patch_login_and_data_unavailable_device(): yield return _patcher() + + +def _patch_login_and_data_auth_failure(): + @contextmanager + def _patcher(): + with ( + patch( + "homeassistant.components.oncue.Oncue.async_login", + side_effect=LoginFailedException, + ), + patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + side_effect=LoginFailedException, + ), + ): + yield + + return _patcher() diff --git a/tests/components/oncue/test_binary_sensor.py b/tests/components/oncue/test_binary_sensor.py index 12ecb19ebc4..d9fce699d39 100644 --- a/tests/components/oncue/test_binary_sensor.py +++ b/tests/components/oncue/test_binary_sensor.py @@ -25,7 +25,7 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: with _patch_login_and_data(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.states.async_all("binary_sensor")) == 1 assert ( @@ -47,7 +47,7 @@ async def test_binary_sensors_not_unavailable(hass: HomeAssistant) -> None: with _patch_login_and_data_unavailable(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.states.async_all("binary_sensor")) == 1 assert ( diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index d757adec771..3907242e26c 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -6,6 +6,7 @@ from aiooncue import LoginFailedException from homeassistant import config_entries from homeassistant.components.oncue.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -36,13 +37,13 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "TEST-username", "password": "test-password", } - assert len(mock_setup_entry.mock_calls) == 1 + assert mock_setup_entry.call_count == 1 async def test_form_invalid_auth(hass: HomeAssistant) -> None: @@ -63,8 +64,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -85,7 +86,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -107,7 +108,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -136,5 +137,56 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauth flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "any", + CONF_PASSWORD: "old", + }, + ) + 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.oncue.config_flow.Oncue.async_login", + side_effect=LoginFailedException, + ): + result2 = await hass.config_entries.flow.async_configure( + flow["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"password": "invalid_auth"} + + 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( + flow["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert config_entry.data[CONF_PASSWORD] == "test-password" + assert mock_setup_entry.call_count == 1 diff --git a/tests/components/oncue/test_init.py b/tests/components/oncue/test_init.py index f10d94d719b..cf93b51dee1 100644 --- a/tests/components/oncue/test_init.py +++ b/tests/components/oncue/test_init.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta from unittest.mock import patch from aiooncue import LoginFailedException @@ -12,10 +13,11 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util -from . import _patch_login_and_data +from . import _patch_login_and_data, _patch_login_and_data_auth_failure -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_config_entry_reload(hass: HomeAssistant) -> None: @@ -29,10 +31,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_login_and_data(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is 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 + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_login_error(hass: HomeAssistant) -> None: @@ -49,7 +51,7 @@ async def test_config_entry_login_error(hass: HomeAssistant) -> None: ): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_config_entry_retry_later(hass: HomeAssistant) -> None: @@ -66,4 +68,27 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: ): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_late_auth_failure(hass: HomeAssistant) -> None: + """Test auth fails after already setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "any", CONF_PASSWORD: "any"}, + unique_id="any", + ) + config_entry.add_to_hass(hass) + with _patch_login_and_data(): + await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + with _patch_login_and_data_auth_failure(): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert len(flows) == 1 + flow = flows[0] + assert flow["context"]["source"] == "reauth" diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 13f5a8b944d..c124bab3c48 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -40,7 +40,7 @@ async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_registry = er.async_get(hass) ent = entity_registry.async_get("sensor.my_generator_latest_firmware") @@ -167,7 +167,7 @@ async def test_sensors_unavailable(hass: HomeAssistant, patcher, connections) -> with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.states.async_all("sensor")) == 25 assert ( diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index 53483241c0b..6b8fcbeefea 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.ondilo_ico.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -12,6 +12,7 @@ from homeassistant.components.ondilo_ico.const import ( ) from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index e5f8ac575e9..a1bab9807d5 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -1,6 +1,6 @@ """Constants for 1-Wire integration.""" -from pyownet.protocol import Error as ProtocolError +from pyownet.protocol import ProtocolError from homeassistant.components.onewire.const import Platform @@ -58,6 +58,12 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 248125"}, ], }, + "16.111111111111": { + # Test case for issue #115984, where the device type cannot be read + ATTR_INJECT_READS: [ + ProtocolError(), # read device type + ], + }, "1F.111111111111": { ATTR_INJECT_READS: [ b"DS2409", # read device type diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 3123dfb6a5e..999794ec20d 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -219,6 +219,18 @@ }), ]) # --- +# name: test_binary_sensors[16.111111111111] + list([ + ]) +# --- +# name: test_binary_sensors[16.111111111111].1 + list([ + ]) +# --- +# name: test_binary_sensors[16.111111111111].2 + list([ + ]) +# --- # name: test_binary_sensors[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index aa8c914ece5..59ed167197d 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -278,6 +278,18 @@ }), ]) # --- +# name: test_sensors[16.111111111111] + list([ + ]) +# --- +# name: test_sensors[16.111111111111].1 + list([ + ]) +# --- +# name: test_sensors[16.111111111111].2 + list([ + ]) +# --- # name: test_sensors[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 2ac542d203c..8fd1e2aeef6 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -351,6 +351,18 @@ }), ]) # --- +# name: test_switches[16.111111111111] + list([ + ]) +# --- +# name: test_switches[16.111111111111].1 + list([ + ]) +# --- +# name: test_switches[16.111111111111].2 + list([ + ]) +# --- # name: test_switches[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 980ecb22d32..c147a522a59 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -41,7 +41,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] # Invalid server @@ -54,7 +54,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +67,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1.2.3.4" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -89,7 +89,7 @@ async def test_user_duplicate( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -98,7 +98,7 @@ async def test_user_duplicate( result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_user_options_clear( result["flow_id"], user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -146,7 +146,7 @@ async def test_user_options_empty_selection( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_selection" assert result["errors"] == {"base": "device_not_selected"} @@ -174,7 +174,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"]["sensor_id"] == "28.111111111111" # Verify that the setting for the device comes back as default when no input is given @@ -182,7 +182,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.111111111111"]["precision"] == "temperature" @@ -220,7 +220,7 @@ async def test_user_options_set_multiple( ] }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.222222222222)" @@ -231,7 +231,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.111111111111)" @@ -242,7 +242,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature9"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.222222222222"]["precision"] == "temperature" @@ -262,5 +262,5 @@ async def test_user_options_no_devices( # Verify that first config step comes back with an empty list of possible devices to choose from result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "No configurable devices found." diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index e59db13d3bb..b08615add0e 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.onvif import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_DHCP @@ -107,7 +107,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -127,7 +127,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device" container = result["data_schema"].schema[config_flow.CONF_HOST].container assert len(container) == 3 @@ -141,7 +141,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={config_flow.CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -158,7 +158,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{URN} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: URN, @@ -180,7 +180,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -200,7 +200,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 @@ -209,7 +209,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" @@ -221,7 +221,7 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -241,7 +241,7 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" @@ -266,7 +266,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -287,7 +287,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> ) # It should skip to manual entry if the only devices are already configured - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" result = await hass.config_entries.flow.async_configure( @@ -302,7 +302,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> ) # It should abort if already configured and entered manually - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_flow_manual_entry(hass: HomeAssistant) -> None: @@ -312,7 +312,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -334,7 +334,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -354,7 +354,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{NAME} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: NAME, @@ -371,7 +371,7 @@ async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -403,7 +403,7 @@ async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_h264" @@ -413,7 +413,7 @@ async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -447,7 +447,7 @@ async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_mac" @@ -457,7 +457,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -481,7 +481,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -501,7 +501,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"base": "onvif_error"} assert result["description_placeholders"] == {"error": "camera not ready"} @@ -526,7 +526,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"base": "onvif_error"} assert result["description_placeholders"] == { @@ -567,7 +567,7 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -589,7 +589,7 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -609,7 +609,7 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"password": "auth_failed"} assert result["description_placeholders"] == {"error": "Authority failure"} @@ -651,7 +651,7 @@ async def test_option_flow(hass: HomeAssistant, option_value: bool) -> None: entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "onvif_devices" result = await hass.config_entries.options.async_configure( @@ -664,7 +664,7 @@ async def test_option_flow(hass: HomeAssistant, option_value: bool) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], @@ -691,7 +691,7 @@ async def test_discovered_by_dhcp_updates_host(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY.ip @@ -716,7 +716,7 @@ async def test_discovered_by_dhcp_does_nothing_if_host_is_the_same( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY_SAME_IP.ip @@ -740,7 +740,7 @@ async def test_discovered_by_dhcp_does_not_update_if_already_loaded( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] != DHCP_DISCOVERY.ip @@ -754,7 +754,7 @@ async def test_discovered_by_dhcp_does_not_update_if_no_matching_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -775,7 +775,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert ( _get_schema_default(result["data_schema"].schema, CONF_USERNAME) @@ -804,7 +804,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {config_flow.CONF_PASSWORD: "auth_failed"} assert result2["description_placeholders"] == { @@ -833,7 +833,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert entry.data[config_flow.CONF_USERNAME] == "new-test-username" @@ -850,7 +850,7 @@ async def test_flow_manual_entry_updates_existing_user_password( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -871,7 +871,7 @@ async def test_flow_manual_entry_updates_existing_user_password( result["flow_id"], user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -890,7 +890,7 @@ async def test_flow_manual_entry_updates_existing_user_password( await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[config_flow.CONF_USERNAME] == USERNAME assert entry.data[config_flow.CONF_PASSWORD] == "new_password" @@ -903,7 +903,7 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -925,7 +925,7 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -945,7 +945,7 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"port": "no_onvif_service"} assert result["description_placeholders"] == {} diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index 2eda6a8192b..5ff01da0fe9 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -27,6 +27,6 @@ async def test_full_user_flow( user_input={CONF_ZONE: ENTITY_ID_HOME}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "test home" assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index a8081c01c32..272c23a9510 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -13,6 +14,7 @@ from tests.common import MockConfigEntry def mock_config_entry(hass): """Mock a config entry.""" entry = MockConfigEntry( + title="OpenAI", domain="openai_conversation", data={ "api_key": "bla", @@ -30,3 +32,9 @@ async def mock_init_component(hass, mock_config_entry): ): assert await async_setup_component(hass, "openai_conversation", {}) await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/openai_conversation/snapshots/test_conversation.ambr b/tests/components/openai_conversation/snapshots/test_conversation.ambr new file mode 100644 index 00000000000..1a488bb948c --- /dev/null +++ b/tests/components/openai_conversation/snapshots/test_conversation.ambr @@ -0,0 +1,67 @@ +# serializer version: 1 +# name: test_default_prompt[None] + list([ + dict({ + 'content': ''' + This smart home is controlled by Home Assistant. + + An overview of the areas and the devices in this smart home: + + Test Area: + - Test Device (Test Model) + + Test Area 2: + - Test Device 2 + - Test Device 3 (Test Model 3A) + - Test Device 4 + - 1 (3) + + Answer the user's questions about the world truthfully. + + If the user wants to control a device, reject the request and suggest using the Home Assistant app. + ''', + 'role': 'system', + }), + dict({ + 'content': 'hello', + 'role': 'user', + }), + dict({ + 'content': 'Hello, how can I help you?', + 'role': 'assistant', + }), + ]) +# --- +# name: test_default_prompt[conversation.openai] + list([ + dict({ + 'content': ''' + This smart home is controlled by Home Assistant. + + An overview of the areas and the devices in this smart home: + + Test Area: + - Test Device (Test Model) + + Test Area 2: + - Test Device 2 + - Test Device 3 (Test Model 3A) + - Test Device 4 + - 1 (3) + + Answer the user's questions about the world truthfully. + + If the user wants to control a device, reject the request and suggest using the Home Assistant app. + ''', + 'role': 'system', + }), + dict({ + 'content': 'hello', + 'role': 'user', + }), + dict({ + 'content': 'Hello, how can I help you?', + 'role': 'assistant', + }), + ]) +# --- diff --git a/tests/components/openai_conversation/snapshots/test_init.ambr b/tests/components/openai_conversation/snapshots/test_init.ambr deleted file mode 100644 index bc06f51f416..00000000000 --- a/tests/components/openai_conversation/snapshots/test_init.ambr +++ /dev/null @@ -1,34 +0,0 @@ -# serializer version: 1 -# name: test_default_prompt - list([ - dict({ - 'content': ''' - This smart home is controlled by Home Assistant. - - An overview of the areas and the devices in this smart home: - - Test Area: - - Test Device (Test Model) - - Test Area 2: - - Test Device 2 - - Test Device 3 (Test Model 3A) - - Test Device 4 - - 1 (3) - - Answer the user's questions about the world truthfully. - - If the user wants to control a device, reject the request and suggest using the Home Assistant app. - ''', - 'role': 'system', - }), - dict({ - 'content': 'hello', - 'role': 'user', - }), - dict({ - 'content': 'Hello, how can I help you?', - 'role': 'assistant', - }), - ]) -# --- diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 659b3825472..57f03d0c0bf 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "api_key": "bla", } @@ -72,7 +72,7 @@ async def test_options( }, ) await hass.async_block_till_done() - assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"]["prompt"] == "Speak like a pirate" assert options["data"]["max_tokens"] == 200 assert options["data"][CONF_CHAT_MODEL] == DEFAULT_CHAT_MODEL @@ -113,5 +113,5 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py new file mode 100644 index 00000000000..9e50204cdde --- /dev/null +++ b/tests/components/openai_conversation/test_conversation.py @@ -0,0 +1,196 @@ +"""Tests for the OpenAI integration.""" + +from unittest.mock import AsyncMock, patch + +from httpx import Response +from openai import RateLimitError +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.completion_usage import CompletionUsage +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import conversation +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import area_registry as ar, device_registry as dr, intent + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("agent_id", [None, "conversation.openai"]) +async def test_default_prompt( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + agent_id: str, +) -> None: + """Test that the default prompt works.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) + for i in range(3): + area_registry.async_create(f"{i}Empty Area") + + if agent_id is None: + agent_id = mock_config_entry.entry_id + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "1234")}, + name="Test Device", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + ) + for i in range(3): + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", f"{i}abcd")}, + name="Test Service", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + entry_type=dr.DeviceEntryType.SERVICE, + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "5678")}, + name="Test Device 2", + manufacturer="Test Manufacturer 2", + model="Device 2", + suggested_area="Test Area 2", + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "qwer")}, + name="Test Device 4", + suggested_area="Test Area 2", + ) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876-disabled")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_registry.async_update_device( + device.id, disabled_by=dr.DeviceEntryDisabler.USER + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876-no-name")}, + manufacturer="Test Manufacturer NoName", + model="Test Model NoName", + suggested_area="Test Area 2", + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876-integer-values")}, + name=1, + manufacturer=2, + model=3, + suggested_area="Test Area 2", + ) + with patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="Hello, how can I help you?", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="gpt-3.5-turbo-0613", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ), + ) as mock_create: + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id=agent_id + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert mock_create.mock_calls[0][2]["messages"] == snapshot + + +async def test_error_handling( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test that the default prompt works.""" + with patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + side_effect=RateLimitError( + response=Response(status_code=None, request=""), body=None, message=None + ), + ): + 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( + "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() + 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 OpenAIAgent.""" + agent = conversation.get_agent_manager(hass).async_get_agent( + mock_config_entry.entry_id + ) + assert agent.supported_languages == "*" diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 3a8db2a71c0..773ba3bca06 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,6 +1,6 @@ """Tests for the OpenAI integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from httpx import Response from openai import ( @@ -9,197 +9,17 @@ from openai import ( BadRequestError, RateLimitError, ) -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from openai.types.completion_usage import CompletionUsage from openai.types.image import Image from openai.types.images_response import ImagesResponse import pytest -from syrupy.assertion import SnapshotAssertion -from homeassistant.components import conversation -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import area_registry as ar, device_registry as dr, intent from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_default_prompt( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test that the default prompt works.""" - entry = MockConfigEntry(title=None) - entry.add_to_hass(hass) - for i in range(3): - area_registry.async_create(f"{i}Empty Area") - - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "1234")}, - name="Test Device", - manufacturer="Test Manufacturer", - model="Test Model", - suggested_area="Test Area", - ) - for i in range(3): - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", f"{i}abcd")}, - name="Test Service", - manufacturer="Test Manufacturer", - model="Test Model", - suggested_area="Test Area", - entry_type=dr.DeviceEntryType.SERVICE, - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "5678")}, - name="Test Device 2", - manufacturer="Test Manufacturer 2", - model="Device 2", - suggested_area="Test Area 2", - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876")}, - name="Test Device 3", - manufacturer="Test Manufacturer 3", - model="Test Model 3A", - suggested_area="Test Area 2", - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "qwer")}, - name="Test Device 4", - suggested_area="Test Area 2", - ) - device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876-disabled")}, - name="Test Device 3", - manufacturer="Test Manufacturer 3", - model="Test Model 3A", - suggested_area="Test Area 2", - ) - device_registry.async_update_device( - device.id, disabled_by=dr.DeviceEntryDisabler.USER - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876-no-name")}, - manufacturer="Test Manufacturer NoName", - model="Test Model NoName", - suggested_area="Test Area 2", - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876-integer-values")}, - name=1, - manufacturer=2, - model=3, - suggested_area="Test Area 2", - ) - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - return_value=ChatCompletion( - id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", - choices=[ - Choice( - finish_reason="stop", - index=0, - message=ChatCompletionMessage( - content="Hello, how can I help you?", - role="assistant", - function_call=None, - tool_calls=None, - ), - ) - ], - created=1700000000, - model="gpt-3.5-turbo-0613", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 - ), - ), - ) as mock_create: - result = await conversation.async_converse( - hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id - ) - - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert mock_create.mock_calls[0][2]["messages"] == snapshot - - -async def test_error_handling( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component -) -> None: - """Test that the default prompt works.""" - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - side_effect=RateLimitError( - response=Response(status_code=None, request=""), body=None, message=None - ), - ): - 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( - "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() - 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 OpenAIAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( - mock_config_entry.entry_id - ) - assert agent.supported_languages == "*" - - @pytest.mark.parametrize( ("service_data", "expected_args"), [ diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index e0896f64340..2bc24e6852b 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -38,7 +38,7 @@ async def test_user_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -47,7 +47,7 @@ async def test_user_create_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "USD" assert result["data"] == { "api_key": "test-api-key", @@ -71,7 +71,7 @@ async def test_form_invalid_auth( {"api_key": "bad-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -109,7 +109,7 @@ async def test_form_unknown_error( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -123,7 +123,7 @@ async def test_already_configured_service( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -131,7 +131,7 @@ async def test_already_configured_service( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -141,7 +141,7 @@ async def test_no_currencies(hass: HomeAssistant, currencies: AsyncMock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -160,7 +160,7 @@ async def test_currencies_timeout(hass: HomeAssistant, currencies: AsyncMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "timeout_connect" @@ -188,7 +188,7 @@ async def test_latest_rates_timeout( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout_connect"} @@ -210,7 +210,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( DOMAIN, context=flow_context, data=mock_config_entry.data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_latest_rates_config_flow.side_effect = OpenExchangeRatesAuthError() @@ -222,7 +222,7 @@ async def test_reauth( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} mock_latest_rates_config_flow.side_effect = None @@ -235,6 +235,6 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/opengarage/test_button.py b/tests/components/opengarage/test_button.py index 3742b7c8aec..4ace809f564 100644 --- a/tests/components/opengarage/test_button.py +++ b/tests/components/opengarage/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -import homeassistant.components.button as button +from homeassistant.components import button from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index 7d3e44017b0..be15ea425ae 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "http://1.1.1.1", @@ -63,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +101,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,5 +132,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/openhome/test_config_flow.py b/tests/components/openhome/test_config_flow.py index 4cc5c58dda5..7ab1e69106c 100644 --- a/tests/components/openhome/test_config_flow.py +++ b/tests/components/openhome/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the Openhome config flow module.""" -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.openhome.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN @@ -31,7 +30,7 @@ async def test_ssdp(hass: HomeAssistant) -> None: data=MOCK_DISCOVER, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == {CONF_NAME: MOCK_FRIENDLY_NAME} @@ -55,7 +54,7 @@ async def test_device_exists(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -74,7 +73,7 @@ async def test_missing_udn(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=broken_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -91,7 +90,7 @@ async def test_missing_ssdp_location(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=broken_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -110,7 +109,7 @@ async def test_host_updated(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_SSDP_LOCATION diff --git a/tests/components/opensky/__init__.py b/tests/components/opensky/__init__.py index 668416c6dcb..ab23de9937b 100644 --- a/tests/components/opensky/__init__.py +++ b/tests/components/opensky/__init__.py @@ -1,20 +1,12 @@ """Opensky tests.""" -from unittest.mock import patch +from homeassistant.core import HomeAssistant -from python_opensky import StatesResponse - -from tests.common import load_json_object_fixture +from tests.common import MockConfigEntry -def patch_setup_entry() -> bool: - """Patch interface.""" - return patch( - "homeassistant.components.opensky.async_setup_entry", return_value=True - ) - - -def get_states_response_fixture(fixture: str) -> StatesResponse: - """Return the states response from json.""" - states_json = load_json_object_fixture(fixture) - return StatesResponse.from_api(states_json) +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the integration.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index 835543b632f..665fdd90e69 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,9 +1,10 @@ """Configure tests for the OpenSky integration.""" -from collections.abc import Awaitable, Callable -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch import pytest +from python_opensky import StatesResponse from homeassistant.components.opensky.const import ( CONF_ALTITUDE, @@ -17,14 +18,18 @@ from homeassistant.const import ( CONF_RADIUS, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from . import get_states_response_fixture +from tests.common import MockConfigEntry, load_json_object_fixture -from tests.common import MockConfigEntry -ComponentSetup = Callable[[MockConfigEntry], Awaitable[None]] +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.opensky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry @pytest.fixture(name="config_entry") @@ -81,19 +86,22 @@ def mock_config_entry_authenticated() -> MockConfigEntry: ) -@pytest.fixture(name="setup_integration") -async def mock_setup_integration( - hass: HomeAssistant, -) -> Callable[[MockConfigEntry], Awaitable[None]]: - """Fixture for setting up the component.""" - - async def func(mock_config_entry: MockConfigEntry) -> None: - mock_config_entry.add_to_hass(hass) - with patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states.json"), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - return func +@pytest.fixture +async def opensky_client() -> Generator[AsyncMock, None, None]: + """Mock the OpenSky client.""" + with ( + patch( + "homeassistant.components.opensky.OpenSky", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.opensky.config_flow.OpenSky", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_states.return_value = StatesResponse.from_api( + load_json_object_fixture("states.json", DOMAIN) + ) + client.is_authenticated = False + yield client diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index c3ae876d36e..e30d5ad8475 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -1,12 +1,11 @@ """Test OpenSky config flow.""" from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from python_opensky.exceptions import OpenSkyUnauthenticatedError -from homeassistant import data_entry_flow from homeassistant.components.opensky.const import ( CONF_ALTITUDE, CONF_CONTRIBUTING_USER, @@ -23,39 +22,36 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import get_states_response_fixture, patch_setup_entry -from .conftest import ComponentSetup - from tests.common import MockConfigEntry +from tests.components.opensky import setup_integration -async def test_full_user_flow(hass: HomeAssistant) -> None: +async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: """Test the full user configuration flow.""" - with patch_setup_entry(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) + 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_RADIUS: 10, - CONF_LATITUDE: 0.0, - CONF_LONGITUDE: 0.0, - CONF_ALTITUDE: 0, - }, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "OpenSky" - assert result["data"] == { + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_RADIUS: 10, CONF_LATITUDE: 0.0, CONF_LONGITUDE: 0.0, - } - assert result["options"] == { - CONF_ALTITUDE: 0.0, - CONF_RADIUS: 10.0, - } + CONF_ALTITUDE: 0, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OpenSky" + assert result["data"] == { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } + assert result["options"] == { + CONF_ALTITUDE: 0.0, + CONF_RADIUS: 10.0, + } @pytest.mark.parametrize( @@ -79,92 +75,77 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: ) async def test_options_flow_failures( hass: HomeAssistant, - setup_integration: ComponentSetup, + mock_setup_entry: AsyncMock, + opensky_client: AsyncMock, config_entry: MockConfigEntry, user_input: dict[str, Any], error: str, ) -> None: """Test load and unload entry.""" - await setup_integration(config_entry) - entry = hass.config_entries.async_entries(DOMAIN)[0] - with patch( - "python_opensky.OpenSky.authenticate", - side_effect=OpenSkyUnauthenticatedError(), - ): - result = await hass.config_entries.options.async_init(entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, config_entry) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" + opensky_client.authenticate.side_effect = OpenSkyUnauthenticatedError + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_RADIUS: 10000, **user_input}, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" - 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"), - ), - ): - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_RADIUS: 10000, - CONF_USERNAME: "homeassistant", - CONF_PASSWORD: "secret", - CONF_CONTRIBUTING_USER: True, - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_RADIUS: 10000, **user_input}, + ) + await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"]["base"] == error + opensky_client.authenticate.side_effect = None + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ CONF_RADIUS: 10000, CONF_USERNAME: "homeassistant", CONF_PASSWORD: "secret", CONF_CONTRIBUTING_USER: True, - } + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + } async def test_options_flow( hass: HomeAssistant, - setup_integration: ComponentSetup, + mock_setup_entry: AsyncMock, + opensky_client: AsyncMock, config_entry: MockConfigEntry, ) -> None: """Test options flow.""" - await setup_integration(config_entry) - entry = hass.config_entries.async_entries(DOMAIN)[0] - result = await hass.config_entries.options.async_init(entry.entry_id) + await setup_integration(hass, config_entry) + result = await hass.config_entries.options.async_init(config_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"), - ), - ): - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_RADIUS: 10000, - CONF_USERNAME: "homeassistant", - CONF_PASSWORD: "secret", - CONF_CONTRIBUTING_USER: True, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ CONF_RADIUS: 10000, CONF_USERNAME: "homeassistant", CONF_PASSWORD: "secret", CONF_CONTRIBUTING_USER: True, - } + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + } diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py index a9e1668d026..f5acf7479a2 100644 --- a/tests/components/opensky/test_init.py +++ b/tests/components/opensky/test_init.py @@ -2,67 +2,59 @@ from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock from python_opensky import OpenSkyError from python_opensky.exceptions import OpenSkyUnauthenticatedError -from homeassistant.components.opensky.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component - -from .conftest import ComponentSetup from tests.common import MockConfigEntry +from tests.components.opensky import setup_integration async def test_load_unload_entry( hass: HomeAssistant, - setup_integration: ComponentSetup, config_entry: MockConfigEntry, + opensky_client: AsyncMock, ) -> None: """Test load and unload entry.""" - await setup_integration(config_entry) - entry = hass.config_entries.async_entries(DOMAIN)[0] + await setup_integration(hass, config_entry) - state = hass.states.get("sensor.opensky") - assert state + assert config_entry.state is ConfigEntryState.LOADED - await hass.config_entries.async_remove(entry.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.opensky") - assert not state + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_load_entry_failure( hass: HomeAssistant, config_entry: MockConfigEntry, + opensky_client: AsyncMock, ) -> None: """Test failure while loading.""" + opensky_client.get_states.side_effect = OpenSkyError() config_entry.add_to_hass(hass) - with patch( - "python_opensky.OpenSky.get_states", - side_effect=OpenSkyError(), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.SETUP_RETRY + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_load_entry_authentication_failure( hass: HomeAssistant, config_entry_authenticated: MockConfigEntry, + opensky_client: AsyncMock, ) -> None: """Test auth failure while loading.""" + opensky_client.authenticate.side_effect = OpenSkyUnauthenticatedError() config_entry_authenticated.add_to_hass(hass) - with patch( - "python_opensky.OpenSky.authenticate", - side_effect=OpenSkyUnauthenticatedError(), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.SETUP_RETRY + assert not await hass.config_entries.async_setup( + config_entry_authenticated.entry_id + ) + await hass.async_block_till_done() + + assert config_entry_authenticated.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index df4faaa3e4a..801980ec5b9 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -1,31 +1,35 @@ """OpenSky sensor tests.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory +from python_opensky import StatesResponse from syrupy import SnapshotAssertion from homeassistant.components.opensky.const import ( + DOMAIN, EVENT_OPENSKY_ENTRY, EVENT_OPENSKY_EXIT, ) from homeassistant.core import Event, HomeAssistant -from . import get_states_response_fixture -from .conftest import ComponentSetup - -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) +from tests.components.opensky import setup_integration async def test_sensor( hass: HomeAssistant, config_entry: MockConfigEntry, - setup_integration: ComponentSetup, snapshot: SnapshotAssertion, + opensky_client: AsyncMock, ): """Test setup sensor.""" - await setup_integration(config_entry) + await setup_integration(hass, config_entry) state = hass.states.get("sensor.opensky") assert state == snapshot @@ -42,11 +46,11 @@ async def test_sensor( async def test_sensor_altitude( hass: HomeAssistant, config_entry_altitude: MockConfigEntry, - setup_integration: ComponentSetup, + opensky_client: AsyncMock, snapshot: SnapshotAssertion, ): """Test setup sensor with a set altitude.""" - await setup_integration(config_entry_altitude) + await setup_integration(hass, config_entry_altitude) state = hass.states.get("sensor.opensky") assert state == snapshot @@ -55,12 +59,12 @@ async def test_sensor_altitude( async def test_sensor_updating( hass: HomeAssistant, config_entry: MockConfigEntry, + opensky_client: AsyncMock, freezer: FrozenDateTimeFactory, - setup_integration: ComponentSetup, snapshot: SnapshotAssertion, ): """Test updating sensor.""" - await setup_integration(config_entry) + await setup_integration(hass, config_entry) events = [] @@ -77,13 +81,11 @@ async def test_sensor_updating( assert events == snapshot - with patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states_1.json"), - ): - await skip_time_and_check_events() - with patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states.json"), - ): - await skip_time_and_check_events() + opensky_client.get_states.return_value = StatesResponse.from_api( + load_json_object_fixture("states_1.json", DOMAIN) + ) + await skip_time_and_check_events() + opensky_client.get_states.return_value = StatesResponse.from_api( + load_json_object_fixture("states.json", DOMAIN) + ) + await skip_time_and_check_events() diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index c92f23f46b4..24b41df8124 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.opentherm_gw.const import ( CONF_FLOOR_TEMP, CONF_PRECISION, @@ -22,6 +22,7 @@ from homeassistant.const import ( PRECISION_TENTHS, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -34,7 +35,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -59,7 +60,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test Entry 1" assert result2["data"] == { CONF_NAME: "Test Entry 1", @@ -98,7 +99,7 @@ async def test_form_import(hass: HomeAssistant) -> None: data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "legacy_gateway" assert result["data"] == { CONF_NAME: "legacy_gateway", @@ -149,10 +150,10 @@ async def test_form_duplicate_entries(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"} ) - assert result1["type"] == "create_entry" - assert result2["type"] == "form" + assert result1["type"] is FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "id_exists"} - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "already_configured"} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -177,7 +178,7 @@ async def test_form_connection_timeout(hass: HomeAssistant) -> None: {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 @@ -198,7 +199,7 @@ async def test_form_connection_error(hass: HomeAssistant) -> None: result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_connect.mock_calls) == 1 @@ -241,7 +242,7 @@ async def test_options_migration(hass: HomeAssistant) -> None: entry.entry_id, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -249,7 +250,7 @@ async def test_options_migration(hass: HomeAssistant) -> None: user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_FLOOR_TEMP] is True @@ -281,7 +282,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -294,7 +295,7 @@ async def test_options_form(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_HALVES assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -308,7 +309,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_READ_PRECISION: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == 0.0 assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -328,7 +329,7 @@ async def test_options_form(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is False diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index db71b712fd9..3d31cf53250 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -6,7 +6,6 @@ from pyopenuv.errors import InvalidApiKeyError import pytest import voluptuous as vol -from homeassistant import data_entry_flow from homeassistant.components.openuv import CONF_FROM_WINDOW, CONF_TO_WINDOW, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( @@ -16,6 +15,7 @@ from homeassistant.const import ( CONF_LONGITUDE, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_API_KEY, TEST_ELEVATION, TEST_LATITUDE, TEST_LONGITUDE @@ -27,7 +27,7 @@ async def test_create_entry(hass: HomeAssistant, client, config, mock_pyopenuv) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test an error occurring: @@ -35,7 +35,7 @@ async def test_create_entry(hass: HomeAssistant, client, config, mock_pyopenuv) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -43,7 +43,7 @@ async def test_create_entry(hass: HomeAssistant, client, config, mock_pyopenuv) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_LATITUDE}, {TEST_LONGITUDE}" assert result["data"] == { CONF_API_KEY: TEST_API_KEY, @@ -60,7 +60,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -69,7 +69,7 @@ async def test_options_flow( ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" def get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: @@ -89,12 +89,12 @@ async def test_options_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} # Subsequent schema uses previous input for suggested values: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert get_schema_marker(result["data_schema"], CONF_FROM_WINDOW).description == { "suggested_value": 3.5 @@ -114,12 +114,12 @@ async def test_step_reauth( assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 77a10c5b26f..2715d83f4f0 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch from pyowm.commons.exceptions import APIRequestError, UnauthorizedError -from homeassistant import data_entry_flow from homeassistant.components.openweathermap.const import ( DEFAULT_FORECAST_MODE, DEFAULT_LANGUAGE, @@ -20,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -59,13 +59,13 @@ async def test_form(hass: HomeAssistant) -> None: conf_entries = hass.config_entries.async_entries(DOMAIN) entry = conf_entries[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(conf_entries[0].entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -88,18 +88,18 @@ async def test_form_options(hass: HomeAssistant) -> None: assert 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 is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "daily"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, @@ -107,18 +107,18 @@ async def test_form_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "onecall_daily"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "onecall_daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, @@ -126,7 +126,7 @@ async def test_form_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_form_invalid_api_key(hass: HomeAssistant) -> None: diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index cb796d8e255..18a7caf23df 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -58,7 +58,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username)" assert result2["data"] == { "utility": "Pacific Gas and Electric Company (PG&E)", @@ -76,7 +76,7 @@ async def test_form_with_mfa( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -87,7 +87,7 @@ async def test_form_with_mfa( "password": "test-password", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert not result2["errors"] with patch( @@ -100,7 +100,7 @@ async def test_form_with_mfa( }, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Consolidated Edison (ConEd) (test-username)" assert result3["data"] == { "utility": "Consolidated Edison (ConEd)", @@ -119,7 +119,7 @@ async def test_form_with_mfa_bad_secret( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -130,7 +130,7 @@ async def test_form_with_mfa_bad_secret( "password": "test-password", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert not result2["errors"] with patch( @@ -144,7 +144,7 @@ async def test_form_with_mfa_bad_secret( }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "base": "invalid_auth", } @@ -161,7 +161,7 @@ async def test_form_with_mfa_bad_secret( }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Consolidated Edison (ConEd) (test-username)" assert result4["data"] == { "utility": "Consolidated Edison (ConEd)", @@ -201,7 +201,7 @@ async def test_form_exceptions( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": expected_error} assert mock_login.call_count == 1 @@ -228,7 +228,7 @@ async def test_form_already_configured( }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert mock_login.call_count == 0 @@ -257,7 +257,7 @@ async def test_form_not_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert ( result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username2)" ) @@ -279,6 +279,7 @@ async def test_form_valid_reauth( ) -> None: """Test that we can handle a valid reauth.""" mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() @@ -297,7 +298,7 @@ async def test_form_valid_reauth( {"username": "test-username", "password": "test-password2"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -328,6 +329,7 @@ async def test_form_valid_reauth_with_mfa( }, ) mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() @@ -347,7 +349,7 @@ async def test_form_valid_reauth_with_mfa( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py index 197da4264d1..dee16cd0632 100644 --- a/tests/components/oralb/test_config_flow.py +++ b/tests/components/oralb/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Series 7000 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" @@ -40,13 +40,13 @@ async def test_async_step_bluetooth_valid_io_series4_device( context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_IO_SERIES_4_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "IO Series 4 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" @@ -59,7 +59,7 @@ async def test_async_step_bluetooth_not_oralb(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -69,7 +69,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -83,14 +83,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "78:DB:2F:C2:48:BE"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Series 7000 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" @@ -106,7 +106,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -120,7 +120,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "78:DB:2F:C2:48:BE"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -142,7 +142,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -159,7 +159,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -170,7 +170,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -178,7 +178,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -191,7 +191,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -202,14 +202,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "78:DB:2F:C2:48:BE"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Series 7000 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" diff --git a/tests/components/osoenergy/test_config_flow.py b/tests/components/osoenergy/test_config_flow.py index c88035eef28..d9db5888cc3 100644 --- a/tests/components/osoenergy/test_config_flow.py +++ b/tests/components/osoenergy/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from apyosoenergyapi.helper import osoenergy_exceptions -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.osoenergy.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +24,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -41,7 +42,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USER_EMAIL assert result2["data"] == { CONF_API_KEY: SUBSCRIPTION_KEY, @@ -74,7 +75,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with patch( @@ -90,7 +91,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_config.data.get(CONF_API_KEY) == SUBSCRIPTION_KEY - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -116,7 +117,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -126,7 +127,7 @@ async def test_user_flow_invalid_subscription_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -138,7 +139,7 @@ async def test_user_flow_invalid_subscription_key(hass: HomeAssistant) -> None: {CONF_API_KEY: SUBSCRIPTION_KEY}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -151,7 +152,7 @@ async def test_user_flow_exception_on_subscription_key_check( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -163,6 +164,6 @@ async def test_user_flow_exception_on_subscription_key_check( {CONF_API_KEY: SUBSCRIPTION_KEY}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index 81dcb894be6..224f77931e5 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -61,7 +61,7 @@ async def test_user_flow( expected_data = {"url": url} - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -74,7 +74,7 @@ async def test_user_flow( "url": url, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} @@ -102,7 +102,7 @@ async def test_user_flow_router_not_setup( result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -140,7 +140,7 @@ async def test_user_flow_router_not_setup( "url": "http://custom_url:1234", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} @@ -163,7 +163,7 @@ async def test_user_flow_404( otbr.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_user_flow_404( "url": url, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -190,7 +190,7 @@ async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None: otbr.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("python_otbr_api.OTBR.get_active_dataset_tlvs", side_effect=error): @@ -200,7 +200,7 @@ async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None: "url": "http://custom_url:1234", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -223,7 +223,7 @@ async def test_hassio_discovery_flow( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -267,7 +267,7 @@ async def test_hassio_discovery_flow_yellow( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert result["data"] == expected_data assert result["options"] == {} @@ -325,7 +325,7 @@ async def test_hassio_discovery_flow_sky_connect( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == expected_data assert result["options"] == {} @@ -396,13 +396,13 @@ async def test_hassio_discovery_flow_2x_addons( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert results[0]["type"] == FlowResultType.CREATE_ENTRY + assert results[0]["type"] is FlowResultType.CREATE_ENTRY assert ( results[0]["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert results[0]["data"] == expected_data assert results[0]["options"] == {} - assert results[1]["type"] == FlowResultType.ABORT + assert results[1]["type"] is FlowResultType.ABORT assert results[1]["reason"] == "single_instance_allowed" assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -460,7 +460,7 @@ async def test_hassio_discovery_flow_router_not_setup( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -512,7 +512,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -575,7 +575,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -598,7 +598,7 @@ async def test_hassio_discovery_flow_404( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -624,7 +624,7 @@ async def test_hassio_discovery_flow_new_port_missing_unique_id( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" expected_data = { @@ -655,7 +655,7 @@ async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None: otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" expected_data = { @@ -683,7 +683,7 @@ async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) - otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Make sure the data was not updated @@ -718,6 +718,6 @@ async def test_config_flow_single_entry( otbr.DOMAIN, context={"source": source}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/ourgroceries/test_config_flow.py b/tests/components/ourgroceries/test_config_flow.py index 0eb17cd4ff6..b18fd699c9a 100644 --- a/tests/components/ourgroceries/test_config_flow.py +++ b/tests/components/ourgroceries/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -73,7 +73,7 @@ async def test_form_error( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} with patch( "homeassistant.components.ourgroceries.config_flow.OurGroceries.login", @@ -87,7 +87,7 @@ async def test_form_error( }, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-username" assert result3["data"] == { "username": "test-username", diff --git a/tests/components/ourgroceries/test_init.py b/tests/components/ourgroceries/test_init.py index ae8452652ae..99b9204ea2b 100644 --- a/tests/components/ourgroceries/test_init.py +++ b/tests/components/ourgroceries/test_init.py @@ -21,10 +21,10 @@ async def test_load_unload( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert ourgroceries_config_entry.state == ConfigEntryState.LOADED + assert ourgroceries_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(ourgroceries_config_entry.entry_id) - assert ourgroceries_config_entry.state == ConfigEntryState.NOT_LOADED + assert ourgroceries_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.fixture diff --git a/tests/components/overkiz/__init__.py b/tests/components/overkiz/__init__.py index e7d729ea41c..48d6055f23a 100644 --- a/tests/components/overkiz/__init__.py +++ b/tests/components/overkiz/__init__.py @@ -11,6 +11,4 @@ def load_setup_fixture( ) -> Setup: """Return setup from fixture.""" setup_json = load_json_object_fixture(fixture) - setup = Setup(**humps.decamelize(setup_json)) - - return setup + return Setup(**humps.decamelize(setup_json)) diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index dbe8c690bc4..50870ae85fe 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -16,11 +16,12 @@ from pyoverkiz.exceptions import ( ) import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.overkiz.const import DOMAIN from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -78,14 +79,14 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -93,7 +94,7 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -121,14 +122,14 @@ async def test_form_only_cloud_supported( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER2}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cloud" with ( @@ -156,14 +157,14 @@ async def test_form_local_happy_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -171,7 +172,7 @@ async def test_form_local_happy_flow( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -217,14 +218,14 @@ async def test_form_invalid_auth_cloud( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -232,7 +233,7 @@ async def test_form_invalid_auth_cloud( {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): @@ -243,7 +244,7 @@ async def test_form_invalid_auth_cloud( await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": error} @@ -273,14 +274,14 @@ async def test_form_invalid_auth_local( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -288,7 +289,7 @@ async def test_form_invalid_auth_local( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): @@ -304,7 +305,7 @@ async def test_form_invalid_auth_local( await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": error} @@ -316,14 +317,14 @@ async def test_form_local_developer_mode_disabled( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -331,7 +332,7 @@ async def test_form_local_developer_mode_disabled( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -350,7 +351,7 @@ async def test_form_local_developer_mode_disabled( }, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": "developer_mode_disabled"} @@ -368,14 +369,14 @@ async def test_form_invalid_cozytouch_auth( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER_COZYTOUCH}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cloud" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): @@ -386,7 +387,7 @@ async def test_form_invalid_cozytouch_auth( await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": error} assert result3["step_id"] == "cloud" @@ -406,14 +407,14 @@ async def test_cloud_abort_on_duplicate_entry( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -421,7 +422,7 @@ async def test_cloud_abort_on_duplicate_entry( {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -436,7 +437,7 @@ async def test_cloud_abort_on_duplicate_entry( {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] == data_entry_flow.FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -460,14 +461,14 @@ async def test_local_abort_on_duplicate_entry( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -475,7 +476,7 @@ async def test_local_abort_on_duplicate_entry( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -496,7 +497,7 @@ async def test_local_abort_on_duplicate_entry( }, ) - assert result4["type"] == data_entry_flow.FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -516,14 +517,14 @@ async def test_cloud_allow_multiple_unique_entries( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -531,7 +532,7 @@ async def test_cloud_allow_multiple_unique_entries( {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -546,7 +547,7 @@ async def test_cloud_allow_multiple_unique_entries( {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == TEST_EMAIL assert result4["data"] == { "api_type": "cloud", @@ -582,7 +583,7 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" with ( @@ -600,7 +601,7 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -632,7 +633,7 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" with ( @@ -650,7 +651,7 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_wrong_account" @@ -681,7 +682,7 @@ async def test_local_reauth_success(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "local_or_cloud" result2 = await hass.config_entries.flow.async_configure( @@ -707,7 +708,7 @@ async def test_local_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -739,7 +740,7 @@ async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None: }, data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "local_or_cloud" result2 = await hass.config_entries.flow.async_configure( @@ -765,7 +766,7 @@ async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_wrong_account" @@ -781,7 +782,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -789,7 +790,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" await hass.config_entries.flow.async_configure( @@ -809,7 +810,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No }, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == TEST_EMAIL assert result4["data"] == { "username": TEST_EMAIL, @@ -840,7 +841,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -852,7 +853,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -860,7 +861,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -868,7 +869,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -883,7 +884,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == TEST_EMAIL assert result4["data"] == { "username": TEST_EMAIL, @@ -905,7 +906,7 @@ async def test_local_zeroconf_flow( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -913,7 +914,7 @@ async def test_local_zeroconf_flow( {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -921,7 +922,7 @@ async def test_local_zeroconf_flow( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -937,7 +938,7 @@ async def test_local_zeroconf_flow( {"username": TEST_EMAIL, "password": TEST_PASSWORD, "verify_ssl": False}, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "gateway-1234-5678-9123.local:8443" assert result4["data"] == { "username": TEST_EMAIL, @@ -967,5 +968,5 @@ async def test_zeroconf_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 3975be7cf80..00899e745b9 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -4,15 +4,20 @@ from unittest.mock import patch import aiohttp -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.ovo_energy.const import DOMAIN +from homeassistant import config_entries +from homeassistant.components.ovo_energy.const import CONF_ACCOUNT, DOMAIN 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 FIXTURE_REAUTH_INPUT = {CONF_PASSWORD: "something1"} -FIXTURE_USER_INPUT = {CONF_USERNAME: "example@example.com", CONF_PASSWORD: "something"} +FIXTURE_USER_INPUT = { + CONF_USERNAME: "example@example.com", + CONF_PASSWORD: "something", + CONF_ACCOUNT: "123456", +} UNIQUE_ID = "example@example.com" @@ -23,7 +28,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -33,19 +38,24 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", - return_value=False, + with ( + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=False, + ), + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.bootstrap_accounts", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -56,7 +66,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -68,7 +78,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -79,7 +89,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -87,6 +97,9 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", return_value=True, ), + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.bootstrap_accounts", + ), patch( "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", "some_name", @@ -101,7 +114,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -118,7 +131,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -127,7 +140,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "authorization_error"} @@ -144,7 +157,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -153,7 +166,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "connection_error"} @@ -175,7 +188,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "authorization_error"} @@ -195,5 +208,5 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 8b353789c83..818524c1c50 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -4,13 +4,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.owntracks import config_flow from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET from homeassistant.components.owntracks.const import DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -66,11 +67,11 @@ async def test_user(hass: HomeAssistant, webhook_id, secret) -> None: flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OwnTracks" assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID assert result["data"][CONF_SECRET] == SECRET @@ -100,7 +101,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: # Should fail, already setup (flow) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -111,7 +112,7 @@ async def test_user_not_supports_encryption( flow = await init_config_flow(hass) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["description_placeholders"]["secret"] == "Encryption is not supported because nacl is not installed." @@ -169,7 +170,7 @@ async def test_with_cloud_sub(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data["cloudhook"] assert ( @@ -198,5 +199,5 @@ async def test_with_cloud_sub_not_connected(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index ec1af77a646..6f6c2c8f7ec 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -17,7 +17,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with ( @@ -33,7 +33,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={CONF_HOST: "example.com"}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "P1 Monitor" assert result2.get("data") == {CONF_HOST: "example.com"} @@ -53,5 +53,5 @@ async def test_api_error(hass: HomeAssistant) -> None: data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index f9664f5d657..49a2ae6fc90 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.panasonic_viera.const import ( ) from homeassistant.const import CONF_PIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_BASIC_DATA, @@ -32,7 +33,7 @@ async def test_flow_non_encrypted(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=False) @@ -46,7 +47,7 @@ async def test_flow_non_encrypted(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == {**MOCK_CONFIG_DATA, ATTR_DEVICE_INFO: MOCK_DEVICE_INFO} @@ -58,7 +59,7 @@ async def test_flow_not_connected_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -70,7 +71,7 @@ async def test_flow_not_connected_error(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -82,7 +83,7 @@ async def test_flow_unknown_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -94,7 +95,7 @@ async def test_flow_unknown_abort(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -107,7 +108,7 @@ async def test_flow_encrypted_not_connected_pin_code_request( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, request_error=TimeoutError) @@ -121,7 +122,7 @@ async def test_flow_encrypted_not_connected_pin_code_request( {**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -132,7 +133,7 @@ async def test_flow_encrypted_unknown_pin_code_request(hass: HomeAssistant) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, request_error=Exception) @@ -146,7 +147,7 @@ async def test_flow_encrypted_unknown_pin_code_request(hass: HomeAssistant) -> N {**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -157,7 +158,7 @@ async def test_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote( @@ -175,7 +176,7 @@ async def test_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -183,7 +184,7 @@ async def test_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> None: {CONF_PIN: "1234"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { **MOCK_CONFIG_DATA, @@ -199,7 +200,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, authorize_error=SOAPError) @@ -213,7 +214,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass: HomeAssistant) -> Non {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" with patch( @@ -225,7 +226,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass: HomeAssistant) -> Non {CONF_PIN: "0000"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": ERROR_INVALID_PIN_CODE} @@ -237,7 +238,7 @@ async def test_flow_encrypted_not_connected_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, authorize_error=TimeoutError) @@ -251,7 +252,7 @@ async def test_flow_encrypted_not_connected_abort(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -259,7 +260,7 @@ async def test_flow_encrypted_not_connected_abort(hass: HomeAssistant) -> None: {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -270,7 +271,7 @@ async def test_flow_encrypted_unknown_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, authorize_error=Exception) @@ -284,7 +285,7 @@ async def test_flow_encrypted_unknown_abort(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -292,7 +293,7 @@ async def test_flow_encrypted_unknown_abort(hass: HomeAssistant) -> None: {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -311,7 +312,7 @@ async def test_flow_non_encrypted_already_configured_abort(hass: HomeAssistant) data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -330,7 +331,7 @@ async def test_flow_encrypted_already_configured_abort(hass: HomeAssistant) -> N data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -349,7 +350,7 @@ async def test_imported_flow_non_encrypted(hass: HomeAssistant) -> None: data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == {**MOCK_CONFIG_DATA, ATTR_DEVICE_INFO: MOCK_DEVICE_INFO} @@ -373,7 +374,7 @@ async def test_imported_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> No data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -381,7 +382,7 @@ async def test_imported_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> No {CONF_PIN: "1234"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { **MOCK_CONFIG_DATA, @@ -407,7 +408,7 @@ async def test_imported_flow_encrypted_invalid_pin_code_error( data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" with patch( @@ -419,7 +420,7 @@ async def test_imported_flow_encrypted_invalid_pin_code_error( {CONF_PIN: "0000"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": ERROR_INVALID_PIN_CODE} @@ -439,7 +440,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass: HomeAssistant) data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -447,7 +448,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass: HomeAssistant) {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -466,7 +467,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass: HomeAssistant) -> Non data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -474,7 +475,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass: HomeAssistant) -> Non {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -491,7 +492,7 @@ async def test_imported_flow_not_connected_error(hass: HomeAssistant) -> None: data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -509,7 +510,7 @@ async def test_imported_flow_unknown_abort(hass: HomeAssistant) -> None: data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -530,7 +531,7 @@ async def test_imported_flow_non_encrypted_already_configured_abort( data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -551,5 +552,5 @@ async def test_imported_flow_encrypted_already_configured_abort( data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index df178e125e1..112d160fa81 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Philadelphia Outage Count" assert result2["data"] == { "county": "PHILADELPHIA", @@ -46,7 +46,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -70,7 +70,7 @@ async def test_meter_value_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -82,7 +82,7 @@ async def test_meter_value_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "invalid_phone_number"} @@ -92,7 +92,7 @@ async def test_incompatible_meter_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", side_effect=IncompatibleMeterError()): @@ -105,7 +105,7 @@ async def test_incompatible_meter_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incompatible_meter" @@ -114,7 +114,7 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", side_effect=UnresponsiveMeterError()): @@ -127,7 +127,7 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "unresponsive_meter"} @@ -137,7 +137,7 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", side_effect=HttpError): @@ -150,7 +150,7 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "http_error"} @@ -160,7 +160,7 @@ async def test_smart_meter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", return_value=True): @@ -173,7 +173,7 @@ async def test_smart_meter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Philadelphia - 1234567890" assert result["data"]["phone_number"] == "1234567890" assert result["context"]["unique_id"] == "PHILADELPHIA-1234567890" diff --git a/tests/components/peco/test_init.py b/tests/components/peco/test_init.py index 55dc0a15a4b..22d2233093a 100644 --- a/tests/components/peco/test_init.py +++ b/tests/components/peco/test_init.py @@ -51,11 +51,11 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -81,7 +81,7 @@ async def test_update_timeout(hass: HomeAssistant, sensor) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( @@ -106,7 +106,7 @@ async def test_total_update_timeout(hass: HomeAssistant, sensor) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( @@ -132,7 +132,7 @@ async def test_http_error(hass: HomeAssistant, sensor: str) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( @@ -158,7 +158,7 @@ async def test_bad_json(hass: HomeAssistant, sensor: str) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: @@ -192,7 +192,7 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_http_error(hass: HomeAssistant) -> None: @@ -226,7 +226,7 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_bad_json(hass: HomeAssistant) -> None: @@ -260,7 +260,7 @@ async def test_meter_bad_json(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_timeout(hass: HomeAssistant) -> None: @@ -294,7 +294,7 @@ async def test_meter_timeout(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_data(hass: HomeAssistant) -> None: @@ -329,4 +329,4 @@ async def test_meter_data(hass: HomeAssistant) -> None: assert hass.states.get("binary_sensor.meter_status") is not None assert hass.states.get("binary_sensor.meter_status").state == "on" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/peco/test_sensor.py b/tests/components/peco/test_sensor.py index 2546b7c8996..9cbef9fa1e6 100644 --- a/tests/components/peco/test_sensor.py +++ b/tests/components/peco/test_sensor.py @@ -57,7 +57,7 @@ async def test_sensor_available( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED sensor_entity = hass.states.get(f"sensor.total_{sensor}") assert sensor_entity is not None @@ -91,7 +91,7 @@ async def test_sensor_available( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED sensor_entity = hass.states.get(f"sensor.bucks_{sensor}") assert sensor_entity is not None diff --git a/tests/components/pegel_online/test_config_flow.py b/tests/components/pegel_online/test_config_flow.py index fedcba94616..45468917565 100644 --- a/tests/components/pegel_online/test_config_flow.py +++ b/tests/components/pegel_online/test_config_flow.py @@ -33,7 +33,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -48,13 +48,13 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" @@ -75,7 +75,7 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -85,13 +85,13 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +100,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -116,7 +116,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -125,13 +125,13 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" @@ -145,7 +145,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -161,7 +161,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -170,13 +170,13 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" diff --git a/tests/components/permobil/test_config_flow.py b/tests/components/permobil/test_config_flow.py index 5968e247a95..ea39e678459 100644 --- a/tests/components/permobil/test_config_flow.py +++ b/tests/components/permobil/test_config_flow.py @@ -46,7 +46,7 @@ async def test_sucessful_config_flow(hass: HomeAssistant, my_permobil: Mock) -> data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -56,7 +56,7 @@ async def test_sucessful_config_flow(hass: HomeAssistant, my_permobil: Mock) -> user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} # request region code @@ -65,7 +65,7 @@ async def test_sucessful_config_flow(hass: HomeAssistant, my_permobil: Mock) -> user_input={CONF_CODE: MOCK_CODE}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == VALID_DATA @@ -89,7 +89,7 @@ async def test_config_flow_incorrect_code( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -99,7 +99,7 @@ async def test_config_flow_incorrect_code( user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -109,7 +109,7 @@ async def test_config_flow_incorrect_code( result["flow_id"], user_input={CONF_CODE: MOCK_CODE}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"]["base"] == "invalid_code" @@ -134,7 +134,7 @@ async def test_config_flow_unsigned_eula( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -144,7 +144,7 @@ async def test_config_flow_unsigned_eula( user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_config_flow_unsigned_eula( result["flow_id"], user_input={CONF_CODE: MOCK_CODE}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"]["base"] == "unsigned_eula" @@ -170,7 +170,7 @@ async def test_config_flow_unsigned_eula( ) # Now the method should not raise an exception, and you can proceed with your assertions - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == VALID_DATA @@ -195,7 +195,7 @@ async def test_config_flow_incorrect_region( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -206,7 +206,7 @@ async def test_config_flow_incorrect_region( user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"]["base"] == "code_request_error" @@ -232,7 +232,7 @@ async def test_config_flow_region_request_error( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"]["base"] == "region_fetch_error" @@ -260,7 +260,7 @@ async def test_config_flow_invalid_email( data={CONF_EMAIL: INVALID_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER assert result["errors"]["base"] == "invalid_email" @@ -289,7 +289,7 @@ async def test_config_flow_reauth_success( context={"source": "reauth", "entry_id": mock_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -299,7 +299,7 @@ async def test_config_flow_reauth_success( user_input={CONF_CODE: reauth_code}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: MOCK_EMAIL, CONF_REGION: MOCK_URL, @@ -331,7 +331,7 @@ async def test_config_flow_reauth_fail_invalid_code( context={"source": "reauth", "entry_id": mock_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -341,7 +341,7 @@ async def test_config_flow_reauth_fail_invalid_code( user_input={CONF_CODE: reauth_invalid_code}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"]["base"] == "invalid_code" @@ -368,5 +368,5 @@ async def test_config_flow_reauth_fail_code_request( context={"source": "reauth", "entry_id": reauth_entry.entry_id}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 07f1f2be933..d7f539db9cf 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -5,9 +5,10 @@ from unittest.mock import ANY from haphilipsjs import PairingFailure import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_CONFIG, @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -52,7 +53,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Philips TV (1234567890)" assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -78,7 +79,7 @@ async def test_reauth( data=mock_config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -88,7 +89,7 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config_entry.data == MOCK_CONFIG | {"system": mock_tv.system} assert len(mock_setup_entry.mock_calls) == 2 @@ -105,7 +106,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, mock_tv) -> None: result["flow_id"], MOCK_USERINPUT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -120,7 +121,7 @@ async def test_form_unexpected_error(hass: HomeAssistant, mock_tv) -> None: result["flow_id"], MOCK_USERINPUT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -131,7 +132,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -139,7 +140,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) MOCK_USERINPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_tv.setTransport.assert_called_with(True) @@ -178,7 +179,7 @@ async def test_pair_request_failed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -204,14 +205,14 @@ async def test_pair_grant_failed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USERINPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_tv.setTransport.assert_called_with(True) @@ -223,7 +224,7 @@ async def test_pair_grant_failed( result["flow_id"], {"pin": "1234"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"pin": "invalid_pin"} # Test with unexpected failure @@ -255,12 +256,12 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ALLOW_NOTIFY: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ALLOW_NOTIFY: True} diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index 3a59e3c6662..3fbac81acbf 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.philips_js.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 350b8b899d8..3b56305e0fc 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -32,7 +32,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -40,7 +40,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG_FLOW_USER, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] == {} @@ -48,7 +48,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_API_KEY: "some_key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -57,7 +57,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG_FLOW_API_KEY, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONFIG_ENTRY_WITH_API_KEY mock_setup.assert_called_once() @@ -68,7 +68,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -80,7 +80,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -88,7 +88,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG_FLOW_USER, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONFIG_ENTRY_WITHOUT_API_KEY mock_setup.assert_called_once() @@ -101,7 +101,7 @@ async def test_flow_user_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -129,6 +129,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "newkey" diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index ec103e9f3a3..9ba18dac9a9 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -6,10 +6,11 @@ import pytest from python_picnic_api.session import PicnicAuthError import requests -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.picnic.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -41,7 +42,7 @@ async def test_form(hass: HomeAssistant, picnic_api) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant, picnic_api) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Picnic" assert result2["data"] == { CONF_ACCESS_TOKEN: picnic_api().session.auth_token, @@ -87,7 +88,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -110,7 +111,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -133,7 +134,7 @@ async def test_form_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -160,7 +161,7 @@ async def test_form_already_configured(hass: HomeAssistant, picnic_api) -> None: ) await hass.async_block_till_done() - assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT + assert result_configure["type"] is FlowResultType.ABORT assert result_configure["reason"] == "already_configured" @@ -179,7 +180,7 @@ async def test_step_reauth(hass: HomeAssistant, picnic_api) -> None: result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -197,7 +198,7 @@ async def test_step_reauth(hass: HomeAssistant, picnic_api) -> None: await hass.async_block_till_done() # Check that the returned flow has type abort because of successful re-authentication - assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT + assert result_configure["type"] is FlowResultType.ABORT assert result_configure["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -219,7 +220,7 @@ async def test_step_reauth_failed(hass: HomeAssistant) -> None: result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -237,7 +238,7 @@ async def test_step_reauth_failed(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Check that the returned flow has type form with error set - assert result_configure["type"] == "form" + assert result_configure["type"] is FlowResultType.FORM assert result_configure["errors"] == {"base": "invalid_auth"} assert len(hass.config_entries.async_entries()) == 1 @@ -258,7 +259,7 @@ async def test_step_reauth_different_account(hass: HomeAssistant, picnic_api) -> result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -276,7 +277,7 @@ async def test_step_reauth_different_account(hass: HomeAssistant, picnic_api) -> await hass.async_block_till_done() # Check that the returned flow has type form with error set - assert result_configure["type"] == "form" + assert result_configure["type"] is FlowResultType.FORM assert result_configure["errors"] == {"base": "different_account"} assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/pilight/test_sensor.py b/tests/components/pilight/test_sensor.py index 629f0f13de4..97e031736e5 100644 --- a/tests/components/pilight/test_sensor.py +++ b/tests/components/pilight/test_sensor.py @@ -4,8 +4,7 @@ import logging import pytest -from homeassistant.components import pilight -import homeassistant.components.sensor as sensor +from homeassistant.components import pilight, sensor from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/ping/test_config_flow.py b/tests/components/ping/test_config_flow.py index 541bdca8b1e..1f55957410d 100644 --- a/tests/components/ping/test_config_flow.py +++ b/tests/components/ping/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == expected_title assert result["data"] == {} assert result["options"] == { @@ -69,7 +69,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -81,7 +81,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "count": count, "host": "10.10.10.1", @@ -100,7 +100,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test2" assert result["data"] == {CONF_IMPORTED_BY: "binary_sensor"} assert result["options"] == { @@ -117,7 +117,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.10" assert result["data"] == {CONF_IMPORTED_BY: "binary_sensor"} assert result["options"] == { diff --git a/tests/components/pjlink/test_media_player.py b/tests/components/pjlink/test_media_player.py index d44bc942290..b2f250103ad 100644 --- a/tests/components/pjlink/test_media_player.py +++ b/tests/components/pjlink/test_media_player.py @@ -9,7 +9,7 @@ from pypjlink import MUTE_AUDIO from pypjlink.projector import ProjectorError import pytest -import homeassistant.components.media_player as media_player +from homeassistant.components import media_player from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index 7088b672f69..efda354f20d 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyplaato.models.device import PlaatoDeviceType import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.plaato.const import ( CONF_DEVICE_NAME, CONF_DEVICE_TYPE, @@ -47,7 +47,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -62,7 +62,7 @@ async def test_show_config_form_device_type_airlock(hass: HomeAssistant) -> None }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool @@ -76,7 +76,7 @@ async def test_show_config_form_device_type_keg(hass: HomeAssistant) -> None: data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None @@ -91,7 +91,7 @@ async def test_show_config_form_validate_webhook( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -102,7 +102,7 @@ async def test_show_config_form_validate_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -126,7 +126,7 @@ async def test_show_config_form_validate_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "webhook" @@ -139,7 +139,7 @@ async def test_show_config_form_validate_webhook_not_connected( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -150,7 +150,7 @@ async def test_show_config_form_validate_webhook_not_connected( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -174,7 +174,7 @@ async def test_show_config_form_validate_webhook_not_connected( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" @@ -193,7 +193,7 @@ async def test_show_config_form_validate_token(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): @@ -201,7 +201,7 @@ async def test_show_config_form_validate_token(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_TOKEN: "valid_token"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == PlaatoDeviceType.Keg.name assert result["data"] == { CONF_USE_WEBHOOK: False, @@ -228,7 +228,7 @@ async def test_show_config_form_no_cloud_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( @@ -239,7 +239,7 @@ async def test_show_config_form_no_cloud_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "webhook" assert result["errors"] is None @@ -262,14 +262,14 @@ async def test_show_config_form_api_method_no_auth_token( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_auth_token" @@ -287,14 +287,14 @@ async def test_show_config_form_api_method_no_auth_token( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_api_method" @@ -318,7 +318,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -328,7 +328,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 assert len(mock_setup_entry.mock_calls) == 1 @@ -352,7 +352,7 @@ async def test_options_webhook(hass: HomeAssistant, webhook_id) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "webhook" assert result["description_placeholders"] == {"webhook_url": ""} @@ -363,7 +363,7 @@ async def test_options_webhook(hass: HomeAssistant, webhook_id) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index d173544284d..0f79ade2df5 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -import homeassistant.components.plant as plant +from homeassistant.components import plant from homeassistant.components.recorder import Recorder from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index d6c91a9d9a8..7e82b1c9d26 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -599,8 +599,7 @@ def setup_plex_server( websocket_connected(mock_websocket) await hass.async_block_till_done() - plex_server = hass.data[DOMAIN][SERVERS][entry.unique_id] - return plex_server + return hass.data[DOMAIN][SERVERS][entry.unique_id] return _wrapper diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 9cfcda1b29d..5f2531992d4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -56,7 +56,7 @@ async def test_bad_credentials( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -69,14 +69,14 @@ async def test_bad_credentials( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_TOKEN] == "faulty_credentials" @@ -88,7 +88,7 @@ async def test_bad_hostname( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -102,14 +102,14 @@ async def test_bad_hostname( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "not_found" @@ -121,7 +121,7 @@ async def test_unknown_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -132,13 +132,13 @@ async def test_unknown_exception( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -155,7 +155,7 @@ async def test_no_servers_found( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -165,13 +165,13 @@ async def test_no_servers_found( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "no_servers" @@ -186,7 +186,7 @@ async def test_single_available_server( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -196,13 +196,13 @@ async def test_single_available_server( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -230,7 +230,7 @@ async def test_multiple_servers_with_selection( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" requests_mock.get( @@ -244,13 +244,13 @@ async def test_multiple_servers_with_selection( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( @@ -259,7 +259,7 @@ async def test_multiple_servers_with_selection( CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER] }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -295,7 +295,7 @@ async def test_adding_last_unconfigured_server( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" requests_mock.get( @@ -310,13 +310,13 @@ async def test_adding_last_unconfigured_server( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -354,7 +354,7 @@ async def test_all_available_servers_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" requests_mock.get("https://plex.tv/api/v2/user", text=plextv_account) @@ -370,13 +370,13 @@ async def test_all_available_servers_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "all_configured" @@ -388,7 +388,7 @@ async def test_option_flow(hass: HomeAssistant, entry, mock_plex_server) -> None result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plex_mp_settings" result = await hass.config_entries.options.async_configure( @@ -399,7 +399,7 @@ async def test_option_flow(hass: HomeAssistant, entry, mock_plex_server) -> None CONF_MONITORED_USERS: list(mock_plex_server.accounts), }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { Platform.MEDIA_PLAYER: { CONF_USE_EPISODE_ART: True, @@ -422,7 +422,7 @@ async def test_missing_option_flow( result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plex_mp_settings" result = await hass.config_entries.options.async_configure( @@ -433,7 +433,7 @@ async def test_missing_option_flow( CONF_MONITORED_USERS: list(mock_plex_server.accounts), }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { Platform.MEDIA_PLAYER: { CONF_USE_EPISODE_ART: True, @@ -470,7 +470,7 @@ async def test_option_flow_new_users_available( result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plex_mp_settings" multiselect_defaults = result["data_schema"].schema["monitored_users"].options @@ -486,7 +486,7 @@ async def test_external_timed_out( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -496,13 +496,13 @@ async def test_external_timed_out( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "token_request_timeout" @@ -515,7 +515,7 @@ async def test_callback_view( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -525,7 +525,7 @@ async def test_callback_view( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' @@ -552,7 +552,7 @@ async def test_manual_config( config_flow.DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"] is None hass.config_entries.flow.async_abort(result["flow_id"]) @@ -564,7 +564,7 @@ async def test_manual_config( ) assert result["data_schema"] is not None - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_advanced" with patch("plexauth.PlexAuth.initiate_auth"): @@ -572,7 +572,7 @@ async def test_manual_config( result["flow_id"], user_input={"setup_method": AUTOMATIC_SETUP_STRING} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP hass.config_entries.flow.async_abort(result["flow_id"]) # Advanced manual @@ -582,14 +582,14 @@ async def test_manual_config( ) assert result["data_schema"] is not None - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_advanced" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"setup_method": MANUAL_SETUP_STRING} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" MANUAL_SERVER = { @@ -610,7 +610,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER_NO_HOST_OR_TOKEN ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "host_or_token" @@ -622,7 +622,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" @@ -634,7 +634,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" @@ -646,7 +646,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" @@ -659,7 +659,7 @@ async def test_manual_config( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://1.2.3.4:32400" assert result["data"][CONF_SERVER] == "Plex Server 1" @@ -682,14 +682,14 @@ async def test_manual_config_with_token( context={"source": SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_advanced" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"setup_method": MANUAL_SETUP_STRING} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" with ( @@ -700,7 +700,7 @@ async def test_manual_config_with_token( result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY mock_url = "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -722,7 +722,7 @@ async def test_integration_discovery(hass: HomeAssistant) -> None: with patch("homeassistant.components.plex.config_flow.GDM", return_value=mock_gdm): await config_flow.async_discover(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) flows = hass.config_entries.flow.async_progress() @@ -761,13 +761,13 @@ async def test_reauth( 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 + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert result["flow_id"] == flow_id @@ -813,13 +813,13 @@ async def test_reauth_multiple_servers_available( 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 + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["flow_id"] == flow_id assert result["reason"] == "reauth_successful" @@ -840,7 +840,7 @@ async def test_client_request_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -864,7 +864,7 @@ async def test_client_header_issues( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index a1a05db9d9a..a14c65daa43 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -10,7 +10,7 @@ import plexapi import requests import requests_mock -import homeassistant.components.plex.const as const +from homeassistant.components.plex import const from homeassistant.components.plex.models import ( LIVE_TV_SECTION, TRANSIENT_SECTION, @@ -360,6 +360,7 @@ async def test_trigger_reauth( ): trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state is not ConfigEntryState.LOADED diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index ec798af6d03..050bf77dd02 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -68,6 +68,7 @@ async def test_new_ignored_users_available( ) trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) + await hass.async_block_till_done(wait_background_tasks=True) server_id = mock_plex_server.machine_identifier @@ -89,6 +90,7 @@ async def test_new_ignored_users_available( ) await wait_for_debouncer(hass) + await hass.async_block_till_done(wait_background_tasks=True) sensor = hass.states.get("sensor.plex_server_1") assert sensor.state == str(len(active_sessions)) diff --git a/tests/components/plex/test_update.py b/tests/components/plex/test_update.py index a96e0409bbb..942162665af 100644 --- a/tests/components/plex/test_update.py +++ b/tests/components/plex/test_update.py @@ -97,7 +97,7 @@ async def test_plex_update( }, blocking=True, ) - assert apply_mock.called_once + assert apply_mock.call_count == 1 # Failed upgrade request requests_mock.put("/updater/apply", status_code=500) diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index d655f95c79b..d496edb4149 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -25,7 +25,7 @@ "dhw_state": false, "flame_state": false, "heating_state": true, - "slave_boiler_state": false + "secondary_boiler_state": false }, "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 92c95f6c5a9..ef7af8a362b 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -25,7 +25,7 @@ "dhw_state": false, "flame_state": false, "heating_state": false, - "slave_boiler_state": false + "secondary_boiler_state": false }, "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index be400b9bc98..8f2e6a75f3f 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -25,7 +25,7 @@ "dhw_state": false, "flame_state": false, "heating_state": false, - "slave_boiler_state": false + "secondary_boiler_state": false }, "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index f42cde65b39..a875324fc13 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -136,7 +136,6 @@ "gateway": { "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", "item_count": 83, - "notifications": {}, "smile_name": "Stretch" } } diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index aec20bc4a0b..878300bddb4 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry @@ -45,9 +46,7 @@ async def test_anna_climate_binary_sensor_change( assert state assert state.state == STATE_ON - await hass.helpers.entity_component.async_update_entity( - "binary_sensor.opentherm_dhw_state" - ) + await async_update_entity(hass, "binary_sensor.opentherm_dhw_state") state = hass.states.get("binary_sensor.opentherm_dhw_state") assert state diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 6e2f4e63d85..4b7c567baa8 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -120,7 +120,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -133,7 +133,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -167,7 +167,7 @@ async def test_zeroconf_flow( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=discovery, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -177,7 +177,7 @@ async def test_zeroconf_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -202,7 +202,7 @@ async def test_zeroconf_flow_stretch( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -212,7 +212,7 @@ async def test_zeroconf_flow_stretch( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -253,7 +253,7 @@ async def test_zercoconf_discovery_update_configuration( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "0.0.0.0" @@ -264,7 +264,7 @@ async def test_zercoconf_discovery_update_configuration( data=TEST_DISCOVERY, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" @@ -293,7 +293,7 @@ async def test_flow_errors( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -303,7 +303,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": reason} assert result2.get("step_id") == "user" @@ -316,7 +316,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test Smile Name" assert result3.get("data") == { CONF_HOST: TEST_HOST, @@ -341,7 +341,7 @@ async def test_zeroconf_abort_anna_with_existing_config_entries( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY_ANNA, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "anna_with_adam" @@ -352,7 +352,7 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY_ANNA, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" flows_in_progress = hass.config_entries.flow.async_progress() @@ -366,7 +366,7 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: data=TEST_DISCOVERY_ADAM, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" flows_in_progress = hass.config_entries.flow.async_progress() @@ -379,7 +379,7 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY_ANNA, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "anna_with_adam" # Adam should still be there diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 4eb0b2cb56a..b206b36be89 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -12,9 +12,8 @@ from plugwise.exceptions import ( import pytest from homeassistant.components.plugwise.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -22,6 +21,9 @@ from tests.common import MockConfigEntry HEATER_ID = "1cbf783bb11e4a7c8a6843dee3a86927" # Opentherm device_id for migration PLUG_ID = "cd0ddb54ef694e11ac18ed1cbce5dbbd" # VCR device_id for migration +SECONDARY_ID = ( + "1cbf783bb11e4a7c8a6843dee3a86927" # Heater_central device_id for migration +) async def test_load_unload_config_entry( @@ -77,7 +79,7 @@ async def test_gateway_config_entry_not_ready( [ ( { - "domain": SENSOR_DOMAIN, + "domain": Platform.SENSOR, "platform": DOMAIN, "unique_id": f"{HEATER_ID}-outdoor_temperature", "suggested_object_id": f"{HEATER_ID}-outdoor_temperature", @@ -118,7 +120,18 @@ async def test_migrate_unique_id_temperature( [ ( { - "domain": SWITCH_DOMAIN, + "domain": Platform.BINARY_SENSOR, + "platform": DOMAIN, + "unique_id": f"{SECONDARY_ID}-slave_boiler_state", + "suggested_object_id": f"{SECONDARY_ID}-slave_boiler_state", + "disabled_by": None, + }, + f"{SECONDARY_ID}-slave_boiler_state", + f"{SECONDARY_ID}-secondary_boiler_state", + ), + ( + { + "domain": Platform.SWITCH, "platform": DOMAIN, "unique_id": f"{PLUG_ID}-plug", "suggested_object_id": f"{PLUG_ID}-plug", diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index 37934942734..ca7c110c963 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -7,6 +7,7 @@ from requests.exceptions import ConnectTimeout from homeassistant import config_entries from homeassistant.components.plum_lightpad.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -33,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-plum-username" assert result2["data"] == { "username": "test-plum-username", @@ -57,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"username": "test-plum-username", "password": "test-plum-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -84,6 +85,6 @@ async def test_form_one_entry_per_email_allowed(hass: HomeAssistant) -> None: {"username": "test-plum-username", "password": "test-plum-password"}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 67745251bf9..ec71b04b84b 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -4,10 +4,10 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def init_config_flow(hass, side_effect=None): @@ -49,7 +49,7 @@ async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> Non flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_flows" @@ -59,12 +59,12 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -74,18 +74,18 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_pypoint) -> No flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "https://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["refresh_args"] == { CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret", @@ -99,7 +99,7 @@ async def test_step_import(hass: HomeAssistant, mock_pypoint) -> None: flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -111,7 +111,7 @@ async def test_wrong_code_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_error" @@ -120,7 +120,7 @@ async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -129,7 +129,7 @@ async def test_abort_if_timeout_generating_auth_url(hass: HomeAssistant) -> None flow = init_config_flow(hass, side_effect=TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -138,7 +138,7 @@ async def test_abort_if_exception_generating_auth_url(hass: HomeAssistant) -> No flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -147,5 +147,5 @@ async def test_abort_no_code(hass: HomeAssistant) -> None: flow = init_config_flow(hass) result = await flow.async_step_code() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_code" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 7a91e546a59..49f790b5075 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -2,11 +2,11 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.poolsense.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_show_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -31,7 +31,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -50,7 +50,7 @@ async def test_valid_credentials(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 83156ffb170..db0ef2e9884 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -13,6 +13,7 @@ from tesla_powerwall import ( from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -36,7 +37,7 @@ async def test_form_source_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_powerwall = await _mock_powerwall_site_name(hass, "MySite") @@ -57,7 +58,7 @@ async def test_form_source_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "MySite" assert result2["data"] == VALID_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -81,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> None: VALID_CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -104,7 +105,7 @@ async def test_invalid_auth(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -124,7 +125,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant) -> None: result["flow_id"], VALID_CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -147,7 +148,7 @@ async def test_form_wrong_version(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "wrong_version"} @@ -166,7 +167,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -193,7 +194,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: hostname="00GGX", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -212,7 +213,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Some site" assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} assert len(mock_setup_entry.mock_calls) == 1 @@ -235,7 +236,7 @@ async def test_dhcp_discovery_manual_configure(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -254,7 +255,7 @@ async def test_dhcp_discovery_manual_configure(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Some site" assert result2["data"] == VALID_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -277,7 +278,7 @@ async def test_dhcp_discovery_auto_configure(hass: HomeAssistant) -> None: hostname="00GGX", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -296,7 +297,7 @@ async def test_dhcp_discovery_auto_configure(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Some site" assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} assert len(mock_setup_entry.mock_calls) == 1 @@ -321,7 +322,7 @@ async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: hostname="00GGX", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -340,7 +341,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_powerwall = await _mock_powerwall_site_name(hass, "My site") @@ -363,7 +364,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -399,7 +400,7 @@ async def test_dhcp_discovery_update_ip_address(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" @@ -436,7 +437,7 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_fails( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" @@ -473,7 +474,7 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_successful( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" @@ -508,7 +509,7 @@ async def test_dhcp_discovery_updates_unique_id(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN @@ -524,7 +525,7 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( unique_id="1.2.3.4", ) entry.add_to_hass(hass) - entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_ERROR) + entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") with ( @@ -547,7 +548,7 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN @@ -586,7 +587,7 @@ async def test_discovered_wifi_does_not_update_ip_if_is_still_online( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" @@ -635,6 +636,6 @@ async def test_discovered_wifi_does_not_update_ip_online_but_access_denied( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" diff --git a/tests/components/powerwall/test_init.py b/tests/components/powerwall/test_init.py index de8da12ccb5..e271cde0fc4 100644 --- a/tests/components/powerwall/test_init.py +++ b/tests/components/powerwall/test_init.py @@ -66,7 +66,7 @@ async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant) async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress(DOMAIN) assert len(flows) == 1 diff --git a/tests/components/private_ble_device/__init__.py b/tests/components/private_ble_device/__init__.py index 967f422872b..b85f29fc394 100644 --- a/tests/components/private_ble_device/__init__.py +++ b/tests/components/private_ble_device/__init__.py @@ -5,8 +5,8 @@ import time from home_assistant_bluetooth import BluetoothServiceInfoBleak -from homeassistant import config_entries from homeassistant.components.private_ble_device.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -38,7 +38,7 @@ async def async_mock_config_entry(hass: HomeAssistant, irk: str = DUMMY_IRK) -> entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index 2acb89240a1..a8821dddace 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -13,7 +13,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info def assert_form_error(result: FlowResult, key: str, value: str) -> None: """Assert that a flow returned a form error.""" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] assert result["errors"][key] == value @@ -26,7 +26,7 @@ async def test_setup_user_no_bluetooth( const.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bluetooth_not_available" @@ -35,7 +35,7 @@ async def test_invalid_irk(hass: HomeAssistant, enable_bluetooth: None) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"irk": "irk:000000"} @@ -48,7 +48,7 @@ async def test_invalid_irk_base64(hass: HomeAssistant, enable_bluetooth: None) - result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"irk": "Ucredacted4T8n!!ZZZ=="} @@ -61,7 +61,7 @@ async def test_invalid_irk_hex(hass: HomeAssistant, enable_bluetooth: None) -> N result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"irk": "irk:abcdefghi"} @@ -74,7 +74,7 @@ async def test_irk_not_found(hass: HomeAssistant, enable_bluetooth: None) -> Non result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -102,7 +102,7 @@ async def test_flow_works(hass: HomeAssistant, enable_bluetooth: None) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM # Check you can finish the flow with patch( @@ -114,7 +114,7 @@ async def test_flow_works(hass: HomeAssistant, enable_bluetooth: None) -> None: user_input={"irk": "irk:00000000000000000000000000000000"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Test Test" assert result["data"] == {"irk": "00000000000000000000000000000000"} assert result["result"].unique_id == "00000000000000000000000000000000" @@ -141,7 +141,7 @@ async def test_flow_works_by_base64( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM # Check you can finish the flow with patch( @@ -153,7 +153,7 @@ async def test_flow_works_by_base64( user_input={"irk": "AAAAAAAAAAAAAAAAAAAAAA=="}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Test Test" assert result["data"] == {"irk": "00000000000000000000000000000000"} assert result["result"].unique_id == "00000000000000000000000000000000" diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index 93542f90520..189a1ac2377 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.profiler.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +16,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -28,7 +29,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Profiler" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -41,5 +42,5 @@ async def test_form_user_only_once(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 7774adb5208..8dcc6917346 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "relay_modes" assert result2["errors"] == {} @@ -54,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: mock_value_step_rm, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] assert result3["data"]["title"] == "1R & 1IN Board" assert result3["data"]["is_old"] is False @@ -78,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -105,7 +105,7 @@ async def test_form_existing_entry_exception(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -126,6 +126,6 @@ async def test_form_user_exception(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 99b73209ad7..499d1a5df14 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -57,6 +57,7 @@ from homeassistant.const import ( STATE_ON, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, STATE_UNLOCKED, UnitOfEnergy, UnitOfTemperature, @@ -1053,6 +1054,126 @@ async def test_disabling_entity( ) +@pytest.mark.parametrize("namespace", [""]) +async def test_entity_becomes_unavailable_with_export( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + client: ClientSessionGenerator, + sensor_entities: dict[str, er.RegistryEntry], +) -> None: + """Test an entity that becomes unavailable is still exported.""" + data = {**sensor_entities} + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 1.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 1.0' in body + ) + + assert ( + 'sensor_humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + # Make sensor_1 unavailable. + set_state_with_entry( + hass, data["sensor_1"], STATE_UNAVAILABLE, data["sensor_1_attributes"] + ) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + # Check that only the availability changed on sensor_1. + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 2.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 0.0' in body + ) + + # The other sensor should be unchanged. + assert ( + 'sensor_humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + # Bring sensor_1 back and check that it is correct. + set_state_with_entry(hass, data["sensor_1"], 200.0, data["sensor_1_attributes"]) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 200.0' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 3.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 1.0' in body + ) + + @pytest.fixture(name="sensor_entities") async def sensor_fixture( hass: HomeAssistant, entity_registry: er.EntityRegistry diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 3c3c2468696..9362cecc289 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant, mock_list_contracts) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -48,7 +48,7 @@ async def test_form(hass: HomeAssistant, mock_list_contracts) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Contract 123" assert result3["data"] == { "contract": "123", @@ -80,7 +80,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -103,7 +103,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -126,7 +126,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -153,7 +153,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_list_contracts) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -175,7 +175,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_list_contracts) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "country": "PT", @@ -231,5 +231,5 @@ async def test_reauth_flow_error(hass: HomeAssistant, exception, base_error) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == base_error diff --git a/tests/components/proximity/test_config_flow.py b/tests/components/proximity/test_config_flow.py index 1841c10873c..3ed9f5cba27 100644 --- a/tests/components/proximity/test_config_flow.py +++ b/tests/components/proximity/test_config_flow.py @@ -56,7 +56,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -66,7 +66,7 @@ async def test_user_flow( result["flow_id"], user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == expected_result zone = hass.states.get(user_input[CONF_ZONE]) @@ -101,7 +101,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert mock_setup_entry.called result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -111,7 +111,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_TOLERANCE: 1, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config.data == { CONF_ZONE: "zone.home", CONF_TRACKED_ENTITIES: ["device_tracker.test2"], @@ -138,7 +138,7 @@ async def test_import_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_NAME: "home", CONF_ZONE: "zone.home", @@ -182,7 +182,7 @@ async def test_abort_duplicated_entry(hass: HomeAssistant) -> None: result["flow_id"], user_input=DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -229,7 +229,7 @@ async def test_avoid_duplicated_title(hass: HomeAssistant) -> None: CONF_TOLERANCE: 10, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home 2" await hass.async_block_till_done() @@ -246,7 +246,7 @@ async def test_avoid_duplicated_title(hass: HomeAssistant) -> None: CONF_TOLERANCE: 10, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home 4" await hass.async_block_till_done() diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py index 26d1d293efd..a60c592fcab 100644 --- a/tests/components/proximity/test_diagnostics.py +++ b/tests/components/proximity/test_diagnostics.py @@ -67,7 +67,7 @@ async def test_entry_diagnostics( mock_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state == ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED assert await get_diagnostics_for_config_entry( hass, hass_client, mock_entry diff --git a/tests/components/prusalink/test_config_flow.py b/tests/components/prusalink/test_config_flow.py index e7db5b54dac..cc66d25b35d 100644 --- a/tests/components/prusalink/test_config_flow.py +++ b/tests/components/prusalink/test_config_flow.py @@ -14,7 +14,7 @@ async def test_form(hass: HomeAssistant, mock_version_api) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, mock_version_api) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "PrusaXL" assert result2["data"] == { "host": "http://1.1.1.1", @@ -65,7 +65,7 @@ async def test_form_mk3(hass: HomeAssistant, mock_version_api) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -88,7 +88,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -111,7 +111,7 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,7 +132,7 @@ async def test_form_too_low_version(hass: HomeAssistant, mock_version_api) -> No }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "not_supported"} @@ -153,7 +153,7 @@ async def test_form_invalid_version_2(hass: HomeAssistant, mock_version_api) -> }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "not_supported"} @@ -178,7 +178,7 @@ async def test_form_invalid_mk3_server_version( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "not_supported"} @@ -201,5 +201,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index 1160143ea11..2cdc6894eeb 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -25,12 +25,12 @@ async def test_unloading( ) -> None: """Test unloading prusalink.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() > 0 assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED for state in hass.states.async_all(): assert state.state == "unavailable" @@ -42,7 +42,7 @@ async def test_failed_update( ) -> None: """Test failed update marks prusalink unavailable.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED with ( patch( @@ -121,7 +121,7 @@ async def test_migration_from_1_1_to_1_2_outdated_firmware( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert entry.minor_version == 1 assert (DOMAIN, "firmware_5_1_required") in issue_registry.issues @@ -130,7 +130,7 @@ async def test_migration_from_1_1_to_1_2_outdated_firmware( await hass.async_block_till_done() # Integration should be running now, the issue should be gone - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.minor_version == 2 assert (DOMAIN, "firmware_5_1_required") not in issue_registry.issues @@ -149,4 +149,4 @@ async def test_migration_fails_on_future_version( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index db478903d1e..4e0505a8644 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyps4_2ndscreen.errors import CredentialTimeout import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ps4 from homeassistant.components.ps4.config_flow import LOCAL_UDP_PORT from homeassistant.components.ps4.const import ( @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import location from tests.common import MockConfigEntry @@ -105,7 +106,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -113,7 +114,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -123,7 +124,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -136,7 +137,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -149,7 +150,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -157,7 +158,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -168,7 +169,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -182,7 +183,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -207,7 +208,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -215,7 +216,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -226,7 +227,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" # Step Link @@ -240,7 +241,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -263,7 +264,7 @@ async def test_port_bind_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): @@ -271,7 +272,7 @@ async def test_port_bind_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -283,14 +284,14 @@ async def test_duplicate_abort(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -299,7 +300,7 @@ async def test_duplicate_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,14 +314,14 @@ async def test_additional_device(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -336,7 +337,7 @@ async def test_additional_device(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -350,7 +351,7 @@ async def test_0_pin(hass: HomeAssistant) -> None: context={"source": "creds"}, data={}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with ( @@ -365,7 +366,7 @@ async def test_0_pin(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" mock_config = MOCK_CONFIG @@ -390,14 +391,14 @@ async def test_no_devices_found_abort(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): @@ -405,7 +406,7 @@ async def test_no_devices_found_abort(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -415,14 +416,14 @@ async def test_manual_mode(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input: manual, results in Step Link. @@ -433,7 +434,7 @@ async def test_manual_mode(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_MANUAL ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -443,7 +444,7 @@ async def test_credential_abort(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): @@ -451,7 +452,7 @@ async def test_credential_abort(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "credential_error" @@ -461,7 +462,7 @@ async def test_credential_timeout(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): @@ -469,7 +470,7 @@ async def test_credential_timeout(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" assert result["errors"] == {"base": "credential_timeout"} @@ -480,14 +481,14 @@ async def test_wrong_pin_error(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -501,7 +502,7 @@ async def test_wrong_pin_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "login_failed"} @@ -512,14 +513,14 @@ async def test_device_connection_error(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -533,7 +534,7 @@ async def test_device_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "cannot_connect"} @@ -544,20 +545,20 @@ async def test_manual_mode_no_ip_error(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"Config Mode": "Manual Entry"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" assert result["errors"] == {CONF_IP_ADDRESS: "no_ipaddress"} diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 238c3c15112..180f51295ac 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ps4 from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, @@ -27,6 +27,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -47,7 +48,7 @@ MOCK_FLOW_RESULT = { "version": VERSION, "minor_version": 1, "handler": DOMAIN, - "type": data_entry_flow.FlowResultType.CREATE_ENTRY, + "type": FlowResultType.CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, "options": {}, @@ -132,7 +133,7 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 6adcad03016..e0be9d508fc 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -147,9 +147,7 @@ async def setup_mock_component(hass, entry=None): mock_entities = hass.states.async_entity_ids() - mock_entity_id = mock_entities[0] - - return mock_entity_id + return mock_entities[0] async def mock_ddp_response(hass, mock_status_data): diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 596853800aa..4305dab2236 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -25,14 +25,14 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) assert result.get("title") == "Pure Energie Meter" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -64,14 +64,14 @@ async def test_full_zeroconf_flow_implementationn( CONF_NAME: "Pure Energie Meter", } assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2.get("title") == "Pure Energie Meter" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -90,7 +90,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -115,5 +115,5 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index efd0db6fd37..fbfc20fc632 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -5,10 +5,10 @@ from unittest.mock import AsyncMock, patch from aiopurpleair.errors import InvalidApiKeyError, PurpleAirError import pytest -from homeassistant import data_entry_flow from homeassistant.components.purpleair import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from .conftest import TEST_API_KEY, TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2 @@ -46,7 +46,7 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when checking the API key: @@ -54,13 +54,13 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": TEST_API_KEY} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == check_api_key_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": TEST_API_KEY} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "by_coordinates" # Test errors that can arise when searching for nearby sensors: @@ -73,7 +73,7 @@ async def test_create_entry_by_coordinates( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == get_nearby_sensors_errors result = await hass.config_entries.flow.async_configure( @@ -84,7 +84,7 @@ async def test_create_entry_by_coordinates( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_sensor" result = await hass.config_entries.flow.async_configure( @@ -93,7 +93,7 @@ async def test_create_entry_by_coordinates( "sensor_index": str(TEST_SENSOR_INDEX1), }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "abcde" assert result["data"] == { "api_key": TEST_API_KEY, @@ -110,7 +110,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"api_key": TEST_API_KEY} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -140,7 +140,7 @@ async def test_reauth( }, data={"api_key": TEST_API_KEY}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Test errors that can arise when checking the API key: @@ -148,14 +148,14 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": "new_api_key"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == check_api_key_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": "new_api_key"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 # Unload to make sure the update does not run after the @@ -181,13 +181,13 @@ async def test_options_add_sensor( ) -> None: """Test adding a sensor via the options flow (including errors).""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "add_sensor"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" # Test errors that can arise when searching for nearby sensors: @@ -202,7 +202,7 @@ async def test_options_add_sensor( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" result = await hass.config_entries.options.async_configure( @@ -213,7 +213,7 @@ async def test_options_add_sensor( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_sensor" result = await hass.config_entries.options.async_configure( @@ -222,7 +222,7 @@ async def test_options_add_sensor( "sensor_index": str(TEST_SENSOR_INDEX2), }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "sensor_indices": [TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2], } @@ -241,13 +241,13 @@ async def test_options_add_sensor_duplicate( ) -> None: """Test adding a duplicate sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "add_sensor"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" result = await hass.config_entries.options.async_configure( @@ -258,7 +258,7 @@ async def test_options_add_sensor_duplicate( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_sensor" result = await hass.config_entries.options.async_configure( @@ -267,7 +267,7 @@ async def test_options_add_sensor_duplicate( "sensor_index": str(TEST_SENSOR_INDEX1), }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Unload to make sure the update does not run after the # mock is removed. @@ -279,13 +279,13 @@ async def test_options_remove_sensor( ) -> None: """Test removing a sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "remove_sensor"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_sensor" device_registry = dr.async_get(hass) @@ -296,7 +296,7 @@ async def test_options_remove_sensor( result["flow_id"], user_input={"sensor_device_id": device_entry.id}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "sensor_indices": [], } @@ -312,19 +312,19 @@ async def test_options_settings( ) -> None: """Test setting settings via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "settings"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"show_on_map": True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "sensor_indices": [TEST_SENSOR_INDEX1], "show_on_map": True, diff --git a/tests/components/pushbullet/test_config_flow.py b/tests/components/pushbullet/test_config_flow.py index 01c946286b4..0b2efa1d556 100644 --- a/tests/components/pushbullet/test_config_flow.py +++ b/tests/components/pushbullet/test_config_flow.py @@ -35,7 +35,7 @@ async def test_flow_user(hass: HomeAssistant, requests_mock_fixture) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "pushbullet" assert result["data"] == MOCK_CONFIG @@ -60,7 +60,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -85,7 +85,7 @@ async def test_flow_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_config, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -101,7 +101,7 @@ async def test_flow_invalid_key(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -118,6 +118,6 @@ async def test_flow_conn_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/pushbullet/test_init.py b/tests/components/pushbullet/test_init.py index 72672f36176..77df6ffa10b 100644 --- a/tests/components/pushbullet/test_init.py +++ b/tests/components/pushbullet/test_init.py @@ -26,7 +26,7 @@ async def test_async_setup_entry_success( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "homeassistant.components.pushbullet.api.PushBulletNotificationProvider.start" @@ -49,7 +49,7 @@ async def test_setup_entry_failed_invalid_key(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_entry_failed_conn_error(hass: HomeAssistant) -> None: @@ -65,7 +65,7 @@ async def test_setup_entry_failed_conn_error(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_async_unload_entry(hass: HomeAssistant, requests_mock_fixture) -> None: @@ -78,8 +78,8 @@ async def test_async_unload_entry(hass: HomeAssistant, requests_mock_fixture) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index fcaedf2b5a6..14347084288 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -44,7 +44,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Pushover" assert result["data"] == MOCK_CONFIG @@ -66,7 +66,7 @@ async def test_flow_user_key_api_key_exists(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_flow_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_config, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -106,7 +106,7 @@ async def test_flow_invalid_user_key( context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_USER_KEY: "invalid_user_key"} @@ -122,7 +122,7 @@ async def test_flow_invalid_api_key( context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -136,7 +136,7 @@ async def test_flow_conn_err(hass: HomeAssistant, mock_pushover: MagicMock) -> N context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -158,7 +158,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -168,7 +168,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -189,7 +189,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> N data=MOCK_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") @@ -200,7 +200,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> N }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_API_KEY: "invalid_api_key", } @@ -232,7 +232,7 @@ async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -242,5 +242,5 @@ async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 15e537fd41f..c3a653042ce 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -35,7 +35,7 @@ async def test_async_setup_entry_success( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_unique_id_updated(hass: HomeAssistant, mock_pushover: MagicMock) -> None: @@ -44,7 +44,7 @@ async def test_unique_id_updated(hass: HomeAssistant, mock_pushover: MagicMock) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.unique_id is None @@ -60,7 +60,7 @@ async def test_async_setup_entry_failed_invalid_api_key( mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_failed_conn_error( @@ -75,7 +75,7 @@ async def test_async_setup_entry_failed_conn_error( mock_pushover.side_effect = BadAPIRequestError await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_async_setup_entry_failed_json_error( @@ -92,4 +92,4 @@ async def test_async_setup_entry_failed_json_error( ) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 1839a7f51e0..20e99f8e497 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -23,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SYSTEM_ID: 12345, @@ -59,7 +59,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_pvoutput.system.side_effect = PVOutputAuthenticationError @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "invalid_auth"} @@ -87,7 +87,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SYSTEM_ID: 12345, @@ -111,7 +111,7 @@ async def test_connection_error(hass: HomeAssistant, mock_pvoutput: MagicMock) - }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_pvoutput.system.mock_calls) == 1 @@ -137,7 +137,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -159,7 +159,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -168,7 +168,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -201,7 +201,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_pvoutput.system.side_effect = PVOutputAuthenticationError @@ -211,7 +211,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} @@ -225,7 +225,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -253,7 +253,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_pvoutput.system.side_effect = PVOutputConnectionError @@ -263,6 +263,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 2a4c5688b5f..70e25392bb6 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.pvpc_hourly_pricing.const import ( ATTR_POWER, ATTR_POWER_P3, @@ -15,6 +15,7 @@ from homeassistant.components.pvpc_hourly_pricing.const import ( ) from homeassistant.const import CONF_API_TOKEN, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -53,12 +54,12 @@ async def test_config_flow( 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["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.esios_pvpc") @@ -73,11 +74,11 @@ async def test_config_flow( 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["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert pvpc_aioclient_mock.call_count == 1 # Check removal @@ -89,12 +90,12 @@ async def test_config_flow( 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["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.esios_pvpc") @@ -110,21 +111,21 @@ async def test_config_flow( config_entry = current_entries[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_token" assert pvpc_aioclient_mock.call_count == 2 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert pvpc_aioclient_mock.call_count == 2 await hass.async_block_till_done() state = hass.states.get("sensor.esios_pvpc") @@ -154,14 +155,14 @@ async def test_config_flow( # disable api token in options result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert pvpc_aioclient_mock.call_count == 6 await hass.async_block_till_done() assert pvpc_aioclient_mock.call_count == 7 @@ -195,19 +196,19 @@ async def test_reauth( 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["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_token" assert pvpc_aioclient_mock.call_count == 0 result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_token" assert result["errors"]["base"] == "invalid_auth" assert pvpc_aioclient_mock.call_count == 1 @@ -216,7 +217,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = result["result"] assert pvpc_aioclient_mock.call_count == 4 @@ -234,7 +235,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert pvpc_aioclient_mock.call_count == 7 @@ -248,7 +249,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert pvpc_aioclient_mock.call_count == 8 diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 1c6fead6c4a..463d69975b4 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -47,9 +47,9 @@ async def test_setup(hass: HomeAssistant) -> None: ) assert len(mock_ex.mock_calls) == 1 - hass, script, source, data = mock_ex.mock_calls[0][1] + test_hass, script, source, data = mock_ex.mock_calls[0][1] - assert hass is hass + assert test_hass is hass assert script == "hello.py" assert source == "fake source" assert data == {"some": "data"} @@ -143,7 +143,7 @@ raise Exception('boom') 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 + assert "Error executing script" in caplog.text async def test_execute_runtime_error_with_response(hass: HomeAssistant) -> None: diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py index 8a424f5c87b..c52762f24d3 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -40,7 +40,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test flow with connection failure, fail with cannot_connect @@ -53,7 +53,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result["flow_id"], USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -69,7 +69,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result["flow_id"], USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -78,7 +78,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result["flow_id"], USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_URL: "http://localhost:8080", CONF_USERNAME: "user", @@ -96,12 +96,12 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test flow with duplicate config result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/qbittorrent/test_helpers.py b/tests/components/qbittorrent/test_helpers.py new file mode 100644 index 00000000000..b308cd33aec --- /dev/null +++ b/tests/components/qbittorrent/test_helpers.py @@ -0,0 +1,108 @@ +"""Test the qBittorrent helpers.""" + +from homeassistant.components.qbittorrent.helpers import ( + format_progress, + format_torrent, + format_torrents, + format_unix_timestamp, + seconds_to_hhmmss, +) +from homeassistant.core import HomeAssistant + + +async def test_seconds_to_hhmmss( + hass: HomeAssistant, +) -> None: + """Test the seconds_to_hhmmss function.""" + assert seconds_to_hhmmss(8640000) == "None" + assert seconds_to_hhmmss(3661) == "01:01:01" + + +async def test_format_unix_timestamp( + hass: HomeAssistant, +) -> None: + """Test the format_unix_timestamp function.""" + assert format_unix_timestamp(1640995200) == "2022-01-01T00:00:00+00:00" + + +async def test_format_progress( + hass: HomeAssistant, +) -> None: + """Test the format_progress function.""" + assert format_progress({"progress": 0.5}) == "50.00" + + +async def test_format_torrents( + hass: HomeAssistant, +) -> None: + """Test the format_torrents function.""" + torrents_data = [ + { + "name": "torrent1", + "hash": "hash1", + "added_on": 1640995200, + "progress": 0.5, + "state": "paused", + "eta": 86400, + "ratio": 1.0, + }, + { + "name": "torrent2", + "hash": "hash1", + "added_on": 1640995200, + "progress": 0.5, + "state": "paused", + "eta": 86400, + "ratio": 1.0, + }, + ] + + expected_result = { + "torrent1": { + "id": "hash1", + "added_date": "2022-01-01T00:00:00+00:00", + "percent_done": "50.00", + "status": "paused", + "eta": "24:00:00", + "ratio": "1.00", + }, + "torrent2": { + "id": "hash1", + "added_date": "2022-01-01T00:00:00+00:00", + "percent_done": "50.00", + "status": "paused", + "eta": "24:00:00", + "ratio": "1.00", + }, + } + + result = format_torrents(torrents_data) + + assert result == expected_result + + +async def test_format_torrent( + hass: HomeAssistant, +) -> None: + """Test the format_torrent function.""" + torrent_data = { + "hash": "hash1", + "added_on": 1640995200, + "progress": 0.5, + "state": "paused", + "eta": 86400, + "ratio": 1.0, + } + + expected_result = { + "id": "hash1", + "added_date": "2022-01-01T00:00:00+00:00", + "percent_done": "50.00", + "status": "paused", + "eta": "24:00:00", + "ratio": "1.00", + } + + result = format_torrent(torrent_data) + + assert result == expected_result diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py index c5b5dd94cc2..7bcd9c09e68 100644 --- a/tests/components/qingping/test_config_flow.py +++ b/tests/components/qingping/test_config_flow.py @@ -23,7 +23,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -31,7 +31,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Motion & Light EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -50,7 +50,7 @@ async def test_async_step_bluetooth_not_enough_info_at_start( context={"source": config_entries.SOURCE_BLUETOOTH}, data=NO_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -58,7 +58,7 @@ async def test_async_step_bluetooth_not_enough_info_at_start( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Qingping Motion & Light" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -75,7 +75,7 @@ async def test_async_step_bluetooth_not_qingping(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_QINGPING_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -85,7 +85,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -99,7 +99,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -108,7 +108,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Motion & Light EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -124,7 +124,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -140,7 +140,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -162,7 +162,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -179,7 +179,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +190,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -198,7 +198,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -211,7 +211,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -222,7 +222,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -231,7 +231,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Motion & Light EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 522c5fabe90..20659182726 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -169,7 +169,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 diff --git a/tests/components/qnap/test_config_flow.py b/tests/components/qnap/test_config_flow.py index 881086b9e10..57ac67525e2 100644 --- a/tests/components/qnap/test_config_flow.py +++ b/tests/components/qnap/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from requests.exceptions import ConnectTimeout -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.qnap import const from homeassistant.const import ( CONF_HOST, @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_HOST, TEST_PASSWORD, TEST_USERNAME @@ -35,7 +36,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -45,7 +46,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -55,7 +56,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -65,7 +66,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -75,7 +76,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test NAS name" assert result["data"] == { CONF_HOST: "1.2.3.4", diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 26a6581b207..94e80d3cd16 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import MagicMock, patch from aioqsw.const import API_MAC_ADDR, API_PRODUCT, API_RESULT from aioqsw.exceptions import LoginError, QswError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac from .util import CONFIG, LIVE_MOCK, SYSTEM_BOARD_MOCK, USERS_LOGIN_MOCK @@ -53,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -67,7 +68,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == f"QNAP {SYSTEM_BOARD_MOCK[API_RESULT][API_PRODUCT]} {SYSTEM_BOARD_MOCK[API_RESULT][API_MAC_ADDR]}" @@ -102,7 +103,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -120,7 +121,7 @@ async def test_form_unique_id_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_id" @@ -164,7 +165,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with ( @@ -193,7 +194,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, @@ -216,7 +217,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -233,7 +234,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -264,7 +265,7 @@ async def test_dhcp_login_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 4b5867b441b..7ec411d6a48 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -84,7 +84,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -100,7 +100,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_TITLE assert result2["data"] == { CONF_HOST: TEST_HOST, @@ -127,7 +127,7 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -142,7 +142,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_value} @@ -151,7 +151,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -166,7 +166,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -177,7 +177,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -193,7 +193,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_TITLE assert result2["data"] == { CONF_HOST: TEST_NAME + ".local", @@ -207,5 +207,5 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index b7325349746..1eaec1bc46e 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.rachio.const import ( ) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -31,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} rachio_mock = _mock_rachio_return_value( @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myusername" assert result2["data"] == { CONF_API_KEY: "api_key", @@ -87,7 +88,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {CONF_API_KEY: "api_key"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -109,7 +110,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_API_KEY: "api_key"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -129,7 +130,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -154,7 +155,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -180,5 +181,5 @@ async def test_form_homekit_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 0e6f708d329..a29b928b405 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -160,7 +160,7 @@ async def setup_integration( aioclient_mock: AiohttpClientMocker, url: str = URL, api_key: str = API_KEY, - unique_id: str = None, + unique_id: str | None = None, skip_entry_setup: bool = False, connection_error: bool = False, invalid_auth: bool = False, diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index 9733393836a..407b7b50c48 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect( @@ -50,7 +50,7 @@ async def test_cannot_connect( data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -64,7 +64,7 @@ async def test_invalid_auth( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -81,7 +81,7 @@ async def test_wrong_app(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "wrong_app" @@ -98,7 +98,7 @@ async def test_zero_conf_failure(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "zeroconf_failed" @@ -115,7 +115,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -132,7 +132,7 @@ async def test_zero_conf(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -153,14 +153,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry() as mock_setup_entry: @@ -169,7 +169,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | {CONF_API_KEY: "test-api-key-reauth"} @@ -188,7 +188,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry(): @@ -197,7 +197,7 @@ async def test_full_user_flow_implementation( user_input=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["data"][CONF_URL] == "http://192.168.1.189:7887/test" diff --git a/tests/components/radarr/test_init.py b/tests/components/radarr/test_init.py index c4226d3f3fb..10ff196bf17 100644 --- a/tests/components/radarr/test_init.py +++ b/tests/components/radarr/test_init.py @@ -16,7 +16,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test unload.""" entry = await setup_integration(hass, aioclient_mock) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -31,7 +31,7 @@ async def test_async_setup_entry_not_ready( """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" entry = await setup_integration(hass, aioclient_mock, connection_error=True) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -43,7 +43,7 @@ async def test_async_setup_entry_auth_failed( mock_connection_invalid_auth(aioclient_mock) await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 0c0f2f479a8..be492e635ef 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") is None result2 = await hass.config_entries.flow.async_configure( @@ -23,7 +23,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Radio Browser" assert result2.get("data") == {} @@ -42,7 +42,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -54,7 +54,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Radio Browser" assert result.get("data") == {} diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 7729dfb86b7..a188f8fcb70 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import MagicMock, patch from radiotherm import CommonThermostat from radiotherm.validate import RadiothermTstatError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.radiotherm.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -76,7 +77,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -97,7 +98,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -119,7 +120,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -137,7 +138,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -163,7 +164,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -192,7 +193,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -209,7 +210,7 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -230,5 +231,5 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 83a45de93ff..51c1e5dcf9f 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -32,7 +32,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -73,7 +73,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 6258ac56249..1af6ca7ba7f 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -87,7 +87,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.fixture(autouse=True) @@ -125,7 +125,7 @@ def get_events_fixture( ) assert response.status == HTTPStatus.OK results = await response.json() - return [{k: event[k] for k in {"summary", "start", "end"}} for event in results] + return [{k: event[k] for k in ("summary", "start", "end")} for event in results] return _fetch @@ -191,7 +191,7 @@ async def test_event_state( freezer.move_to(freeze_time) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state is not None @@ -295,7 +295,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state is not None diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 09db734f1ad..b4cd51d6b3e 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -61,7 +61,7 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") assert "flow_id" in result @@ -102,7 +102,7 @@ async def test_controller_flow( """Test the controller is setup correctly.""" result = await complete_flow(hass) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == HOST assert "result" in result assert dict(result["result"].data) == expected_config_entry @@ -159,13 +159,13 @@ async def test_multiple_config_entries( ) -> None: """Test setting up multiple config entries that refer to different devices.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) result = await complete_flow(hass) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert dict(result.get("result").data) == expected_config_entry entries = hass.config_entries.async_entries(DOMAIN) @@ -234,14 +234,14 @@ async def test_duplicate_config_entries( ) -> None: """Test that a device can not be registered twice.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) result = await complete_flow(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert dict(config_entry.data) == expected_config_entry_data @@ -261,7 +261,7 @@ async def test_controller_cannot_connect( ) result = await complete_flow(hass) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -279,7 +279,7 @@ async def test_controller_timeout( side_effect=TimeoutError, ): result = await complete_flow(hass) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "timeout_connect"} @@ -291,7 +291,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: # Setup config flow result = await complete_flow(hass) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == HOST assert "result" in result assert result["result"].data == CONFIG_ENTRY_DATA @@ -299,18 +299,18 @@ async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: # Assert single config entry is loaded config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Initiate the options flow result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Change the default duration result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_DURATION: 5} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert config_entry.options == { ATTR_DURATION: 5, } diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 00cbefc6556..5b2e2ea6d1b 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -37,7 +37,7 @@ async def test_init_success( """Test successful setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() @@ -112,17 +112,17 @@ async def test_fix_unique_id( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED assert entries[0].unique_id is None assert entries[0].data.get(CONF_MAC) is None await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Verify config entry now has a unique id entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert entries[0].unique_id == MAC_ADDRESS_UNIQUE_ID assert entries[0].data.get(CONF_MAC) == MAC_ADDRESS @@ -170,7 +170,7 @@ async def test_fix_unique_id_failure( await hass.config_entries.async_setup(config_entry.entry_id) # Config entry is loaded, but not updated - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id is None assert expected_warning in caplog.text @@ -204,7 +204,7 @@ async def test_fix_unique_id_duplicate( responses.extend(responses_copy) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID assert "Unable to fix missing unique id (already exists)" in caplog.text @@ -305,7 +305,7 @@ async def test_fix_entity_unique_ids( ) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_entry = entity_registry.async_get(entity_entry.id) assert entity_entry @@ -421,7 +421,7 @@ async def test_fix_duplicate_device_ids( assert len(device_entries) == 2 await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Only the device with the new format exists device_entries = dr.async_entries_for_config_entry( diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 0830a238fd7..b3a1860baab 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -38,7 +38,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -155,7 +155,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED raindelay = hass.states.get("number.rain_bird_controller_rain_delay") assert raindelay is not None diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index 730e1d50809..34b93f7b411 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -32,7 +32,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -85,7 +85,7 @@ async def test_sensor_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") assert raindelay is not None diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 068fe03ac33..1352a4a633d 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -44,7 +44,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -298,7 +298,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 191a7a4793e..d3df44fb4fe 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "abcdef" assert result2["data"] == { CONF_TYPE: TYPE_EAGLE_200, @@ -76,7 +76,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -99,5 +99,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index d7b188d6b14..d86dee6e0f6 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -6,11 +6,11 @@ from aioraven.device import RAVEnConnectionError import pytest import serial.tools.list_ports -from homeassistant import data_entry_flow from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_MAC, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import create_mock_device from .const import DEVICE_NAME, DISCOVERY_INFO, METER_LIST @@ -74,7 +74,7 @@ async def test_flow_usb(hass: HomeAssistant, mock_comports, mock_device): DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "meters" @@ -83,7 +83,7 @@ async def test_flow_usb(hass: HomeAssistant, mock_comports, mock_device): result["flow_id"], user_input={CONF_MAC: [METER_LIST.meter_mac_ids[0].hex()]} ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY async def test_flow_usb_cannot_connect( @@ -94,7 +94,7 @@ async def test_flow_usb_cannot_connect( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -106,7 +106,7 @@ async def test_flow_usb_timeout_connect( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "timeout_connect" @@ -118,7 +118,7 @@ async def test_flow_usb_comm_error( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -129,7 +129,7 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "user" @@ -141,7 +141,7 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "meters" @@ -150,7 +150,7 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): result["flow_id"], user_input={CONF_MAC: [METER_LIST.meter_mac_ids[0].hex()]} ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY async def test_flow_user_no_available_devices(hass: HomeAssistant, mock_comports): @@ -165,7 +165,7 @@ async def test_flow_user_no_available_devices(hass: HomeAssistant, mock_comports context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_devices_found" @@ -176,7 +176,7 @@ async def test_flow_user_in_progress(hass: HomeAssistant, mock_comports): context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "user" @@ -186,7 +186,7 @@ async def test_flow_user_in_progress(hass: HomeAssistant, mock_comports): context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_in_progress" @@ -202,7 +202,7 @@ async def test_flow_user_cannot_connect( }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_DEVICE: "cannot_connect"} @@ -218,7 +218,7 @@ async def test_flow_user_timeout_connect( }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_DEVICE: "timeout_connect"} @@ -234,5 +234,5 @@ async def test_flow_user_comm_error( }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_DEVICE: "cannot_connect"} diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 1c065a8f7ce..808c2f184a7 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from regenmaschine.errors import RainMachineError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components import zeroconf from homeassistant.components.rainmachine import ( CONF_ALLOW_INACTIVE_ZONES_TO_RUN, @@ -16,6 +16,7 @@ from homeassistant.components.rainmachine import ( ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er @@ -24,7 +25,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -107,7 +108,7 @@ async def test_options_flow(hass: HomeAssistant, config, config_entry) -> None: ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -118,7 +119,7 @@ async def test_options_flow(hass: HomeAssistant, config, config_entry) -> None: CONF_ALLOW_INACTIVE_ZONES_TO_RUN: False, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_DEFAULT_ZONE_RUN_TIME: 600, CONF_USE_APP_RUN_TIMES: False, @@ -133,7 +134,7 @@ async def test_show_form(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -144,7 +145,7 @@ async def test_step_user(hass: HomeAssistant, config, setup_rainmachine) -> None context={"source": config_entries.SOURCE_USER}, data=config, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -179,7 +180,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -207,7 +208,7 @@ async def test_step_homekit_zeroconf_ip_change( ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_IP_ADDRESS] == "192.168.1.2" @@ -236,7 +237,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -258,7 +259,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "12345" assert result2["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -290,7 +291,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -310,5 +311,5 @@ async def test_discovery_by_homekit_and_zeroconf_same_time( ), ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/random/test_config_flow.py b/tests/components/random/test_config_flow.py index 4ffa59da4e4..b4eff5c966b 100644 --- a/tests/components/random/test_config_flow.py +++ b/tests/components/random/test_config_flow.py @@ -60,14 +60,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": entity_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == entity_type with patch( @@ -82,7 +82,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My random entity" assert result["data"] == {} assert result["options"] == { @@ -108,14 +108,14 @@ async def test_wrong_uom( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "sensor"}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "sensor" with pytest.raises(Invalid, match="is not a valid unit for device class"): @@ -179,7 +179,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == entity_type assert "name" not in result["data_schema"].schema @@ -187,7 +187,7 @@ async def test_options( result["flow_id"], user_input=options_options, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My random", "entity_type": entity_type, diff --git a/tests/components/rapt_ble/test_config_flow.py b/tests/components/rapt_ble/test_config_flow.py index b71843bd44f..2189b8a610c 100644 --- a/tests/components/rapt_ble/test_config_flow.py +++ b/tests/components/rapt_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.rapt_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "RAPT Pill 0666" assert result2["data"] == {} assert result2["result"].unique_id == RAPT_MAC @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_rapt(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_RAPT_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.rapt_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": RAPT_MAC}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "RAPT Pill 0666" assert result2["data"] == {} assert result2["result"].unique_id == RAPT_MAC @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": RAPT_MAC}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.rapt_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": RAPT_MAC}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "RAPT Pill 0666" assert result2["data"] == {} assert result2["result"].unique_id == RAPT_MAC diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py index 05fea6ed3d3..19c23295493 100644 --- a/tests/components/raspberry_pi/test_config_flow.py +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -21,7 +21,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Raspberry Pi" assert result["data"] == {} assert result["options"] == {} @@ -54,6 +54,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/raspberry_pi/test_init.py b/tests/components/raspberry_pi/test_init.py index 80b5eedf2af..0c4134bf2be 100644 --- a/tests/components/raspberry_pi/test_init.py +++ b/tests/components/raspberry_pi/test_init.py @@ -118,4 +118,4 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index b8c21be300e..2aa39f2c2d3 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "11-ZKZ-3" assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -45,7 +45,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_rdw_config_flow.vehicle.side_effect = RDWUnknownLicensePlateError @@ -56,7 +56,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "unknown_license_plate"} @@ -68,7 +68,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "11-ZKZ-3" assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -85,5 +85,5 @@ async def test_connection_error( data={CONF_LICENSE_PLATE: "0001TJ"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index a65b0d27a74..aac829f00a3 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, patch from aiorecollect.errors import RecollectError import pytest -from homeassistant import data_entry_flow from homeassistant.components.recollect_waste import ( CONF_PLACE_ID, CONF_SERVICE_ID, @@ -14,6 +13,7 @@ from homeassistant.components.recollect_waste import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_PLACE_ID, TEST_SERVICE_ID @@ -39,7 +39,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when checking the API key: @@ -47,13 +47,13 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == get_pickup_events_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}" assert result["data"] == { CONF_PLACE_ID: TEST_PLACE_ID, @@ -66,7 +66,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,11 +75,11 @@ async def test_options_flow( ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FRIENDLY_NAME: True} diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index e8fd6dbcf53..e0f43323f25 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -109,7 +109,9 @@ async def async_wait_recording_done(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def async_wait_purge_done(hass: HomeAssistant, max: int = None) -> None: +async def async_wait_purge_done( + hass: HomeAssistant, max_number: int | None = None +) -> None: """Wait for max number of purge events. Because a purge may insert another PurgeTask into @@ -117,9 +119,9 @@ async def async_wait_purge_done(hass: HomeAssistant, max: int = None) -> None: a maximum number of WaitTasks that we will put into the queue. """ - if not max: - max = DEFAULT_PURGE_TASKS - for _ in range(max + 1): + if not max_number: + max_number = DEFAULT_PURGE_TASKS + for _ in range(max_number + 1): await async_wait_recording_done(hass) @@ -325,10 +327,10 @@ def convert_pending_states_to_meta(instance: Recorder, session: Session) -> None entity_ids: set[str] = set() states: set[States] = set() states_meta_objects: dict[str, StatesMeta] = {} - for object in session: - if isinstance(object, States): - entity_ids.add(object.entity_id) - states.add(object) + for session_object in session: + if isinstance(session_object, States): + entity_ids.add(session_object.entity_id) + states.add(session_object) entity_id_to_metadata_ids = instance.states_meta_manager.get_many( entity_ids, session, True @@ -352,10 +354,10 @@ def convert_pending_events_to_event_types(instance: Recorder, session: Session) event_types: set[str] = set() events: set[Events] = set() event_types_objects: dict[str, EventTypes] = {} - for object in session: - if isinstance(object, Events): - event_types.add(object.event_type) - events.add(object) + for session_object in session: + if isinstance(session_object, Events): + event_types.add(session_object.event_type) + events.add(session_object) event_type_to_event_type_ids = instance.event_type_manager.get_many( event_types, session, True diff --git a/tests/components/recorder/db_schema_0.py b/tests/components/recorder/db_schema_0.py index 6365ff6a7e7..9062de01b59 100644 --- a/tests/components/recorder/db_schema_0.py +++ b/tests/components/recorder/db_schema_0.py @@ -30,7 +30,7 @@ Base = declarative_base() _LOGGER = logging.getLogger(__name__) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __tablename__ = "events" @@ -66,7 +66,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __tablename__ = "states" @@ -125,7 +125,7 @@ class States(Base): # type: ignore return None -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __tablename__ = "recorder_runs" diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index 4d48400e370..24786b1ad44 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -66,7 +66,7 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = { @@ -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__ = ( # noqa: PIE794 + __table_args__ = ( # type: ignore[assignment] # noqa: PIE794 # Used for fetching events at a specific time # see logbook Index("ix_events_event_type_time_fired", "event_type", "time_fired"), @@ -133,7 +133,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = { @@ -156,7 +156,7 @@ class States(Base): # type: ignore event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) - __table_args__ = ( # noqa: PIE794 + __table_args__ = ( # type: ignore[assignment] # 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"), @@ -217,7 +217,7 @@ class States(Base): # type: ignore return None -class Statistics(Base): # type: ignore +class Statistics(Base): # type: ignore[valid-type,misc] """Statistics.""" __table_args__ = { @@ -237,7 +237,7 @@ class Statistics(Base): # type: ignore state = Column(Float()) sum = Column(Float()) - __table_args__ = ( # noqa: PIE794 + __table_args__ = ( # type: ignore[assignment] # noqa: PIE794 # Used for fetching statistics for a certain entity at a specific time Index("ix_statistics_statistic_id_start", "statistic_id", "start"), ) @@ -253,7 +253,7 @@ class Statistics(Base): # type: ignore ) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __tablename__ = TABLE_RECORDER_RUNS @@ -304,7 +304,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -366,7 +366,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -383,7 +383,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -395,7 +395,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -407,7 +407,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_18.py b/tests/components/recorder/db_schema_18.py index 2ce0dfae5f5..db6fbb78f56 100644 --- a/tests/components/recorder/db_schema_18.py +++ b/tests/components/recorder/db_schema_18.py @@ -68,7 +68,7 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -131,7 +131,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -211,7 +211,7 @@ class States(Base): # type: ignore return None -class Statistics(Base): # type: ignore +class Statistics(Base): # type: ignore[valid-type,misc] """Statistics.""" __table_args__ = ( @@ -244,7 +244,7 @@ class Statistics(Base): # type: ignore ) -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __tablename__ = TABLE_STATISTICS_META @@ -267,7 +267,7 @@ class StatisticsMeta(Base): # type: ignore ) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -317,7 +317,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -379,7 +379,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -396,7 +396,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -408,7 +408,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -420,7 +420,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_22.py b/tests/components/recorder/db_schema_22.py index 0d336c96403..cd0dc52a927 100644 --- a/tests/components/recorder/db_schema_22.py +++ b/tests/components/recorder/db_schema_22.py @@ -84,7 +84,7 @@ DOUBLE_TYPE = ( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -148,7 +148,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -283,13 +283,13 @@ class StatisticsBase: @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[valid-type,misc] """Long term statistics.""" duration = timedelta(hours=1) @@ -301,7 +301,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[valid-type,misc] """Short term statistics.""" duration = timedelta(minutes=5) @@ -322,7 +322,7 @@ class StatisticMetaData(TypedDict): has_sum: bool -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __table_args__ = ( @@ -354,7 +354,7 @@ class StatisticsMeta(Base): # type: ignore ) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -404,7 +404,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -422,7 +422,7 @@ class SchemaChanges(Base): # type: ignore ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[valid-type,misc] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -498,7 +498,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -515,7 +515,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -527,7 +527,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -539,7 +539,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_23.py b/tests/components/recorder/db_schema_23.py index d4b6e8b0a73..9187d271216 100644 --- a/tests/components/recorder/db_schema_23.py +++ b/tests/components/recorder/db_schema_23.py @@ -83,7 +83,7 @@ DOUBLE_TYPE = ( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -147,7 +147,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -282,13 +282,13 @@ class StatisticsBase: @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[valid-type,misc] """Long term statistics.""" duration = timedelta(hours=1) @@ -300,7 +300,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[valid-type,misc] """Short term statistics.""" duration = timedelta(minutes=5) @@ -323,7 +323,7 @@ class StatisticMetaData(TypedDict): unit_of_measurement: str | None -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __table_args__ = ( @@ -344,7 +344,7 @@ class StatisticsMeta(Base): # type: ignore return StatisticsMeta(**meta) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -394,7 +394,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -412,7 +412,7 @@ class SchemaChanges(Base): # type: ignore ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[valid-type,misc] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -488,7 +488,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -505,7 +505,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -517,7 +517,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -529,7 +529,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: 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 6893a7257f4..9f902523c64 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -102,7 +102,7 @@ EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -225,7 +225,7 @@ class EventTypes(Base): # type: ignore[misc,valid-type] event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -406,13 +406,13 @@ class StatisticsBase: @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[valid-type,misc] """Long term statistics.""" duration = timedelta(hours=1) @@ -424,7 +424,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[valid-type,misc] """Short term statistics.""" duration = timedelta(minutes=5) @@ -447,7 +447,7 @@ class StatisticMetaData(TypedDict): unit_of_measurement: str | None -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __table_args__ = ( @@ -468,7 +468,7 @@ class StatisticsMeta(Base): # type: ignore return StatisticsMeta(**meta) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -518,7 +518,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -536,7 +536,7 @@ class SchemaChanges(Base): # type: ignore ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[valid-type,misc] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -612,7 +612,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -629,7 +629,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -641,7 +641,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -653,7 +653,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py index 0d763f91b67..d989cacb76a 100644 --- a/tests/components/recorder/db_schema_25.py +++ b/tests/components/recorder/db_schema_25.py @@ -653,7 +653,7 @@ class LazyState(State): "last_updated": last_updated_isoformat, } - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Return the comparison.""" return ( other.__class__ in [self.__class__, State] diff --git a/tests/components/recorder/db_schema_28.py b/tests/components/recorder/db_schema_28.py index feaf877b36f..8c984b61f6c 100644 --- a/tests/components/recorder/db_schema_28.py +++ b/tests/components/recorder/db_schema_28.py @@ -818,7 +818,7 @@ class LazyState(State): "last_updated": last_updated_isoformat, } - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Return the comparison.""" return ( other.__class__ in [self.__class__, State] diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index d16a6856399..ebcb0522e72 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -554,7 +554,7 @@ def test_get_significant_states_minimal_response( 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( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") @@ -636,13 +636,11 @@ def test_get_significant_states_without_initial( 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], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] del states["thermostat.test3"] diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index cbe4c3ac5c8..2d0b3398a87 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -328,7 +328,7 @@ def test_get_significant_states_minimal_response( 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( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") @@ -384,10 +384,7 @@ def test_get_significant_states_with_initial( if entity_id == "media_player.test": states[entity_id] = states[entity_id][1:] for state in states[entity_id]: - if ( - state.last_changed == one - or state.last_changed == one_with_microsecond - ): + if state.last_changed in (one, one_with_microsecond): state.last_changed = one_and_half state.last_updated = one_and_half @@ -418,13 +415,11 @@ def test_get_significant_states_without_initial( 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], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] hist = history.get_significant_states( diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index b926aa1903b..5acf07b0604 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -327,7 +327,7 @@ def test_get_significant_states_minimal_response( 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( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") @@ -408,13 +408,11 @@ def test_get_significant_states_without_initial( 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], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] hist = history.get_significant_states( diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index 98ed6089de6..e342799c3a8 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -556,7 +556,7 @@ def test_get_significant_states_minimal_response( 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( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") @@ -638,13 +638,11 @@ def test_get_significant_states_without_initial( 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], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] del states["thermostat.test3"] diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b2da3f1d62f..e80bc7ca7d1 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -9,6 +9,7 @@ from freezegun import freeze_time import pytest from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session +from voluptuous.error import MultipleInvalid from homeassistant.components import recorder from homeassistant.components.recorder.const import SupportedDialect @@ -1446,20 +1447,20 @@ async def test_purge_entities( _add_purge_records(hass) - # Confirm calling service without arguments matches all records (default filter behavior) + # Confirm calling service without arguments is invalid with session_scope(hass=hass) as session: states = session.query(States) assert states.count() == 190 - await _purge_entities(hass, [], [], []) + with pytest.raises(MultipleInvalid): + await _purge_entities(hass, [], [], []) with session_scope(hass=hass, read_only=True) as session: states = session.query(States) - assert states.count() == 0 + assert states.count() == 190 - # The states_meta table should be empty states_meta_remain = session.query(StatesMeta) - assert states_meta_remain.count() == 0 + assert states_meta_remain.count() == 4 async def _add_test_states(hass: HomeAssistant, wait_recording_done: bool = True): diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 549280efba2..9e32fa2c500 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1040,14 +1040,14 @@ async def test_resolve_period(hass: HomeAssistant) -> None: def test_chunked_or_all(): """Test chunked_or_all can iterate chunk sizes larger than the passed in collection.""" - all = [] + all_items = [] incoming = (1, 2, 3, 4) for chunk in chunked_or_all(incoming, 2): assert len(chunk) == 2 - all.extend(chunk) - assert all == [1, 2, 3, 4] + all_items.extend(chunk) + assert all_items == [1, 2, 3, 4] - all = [] + all_items = [] incoming = (1, 2, 3, 4) for chunk in chunked_or_all(incoming, 5): assert len(chunk) == 4 @@ -1055,5 +1055,5 @@ def test_chunked_or_all(): # collection since we want to avoid copying the collection # if we don't need to assert chunk is incoming - all.extend(chunk) - assert all == [1, 2, 3, 4] + all_items.extend(chunk) + assert all_items == [1, 2, 3, 4] diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index d594218e9d4..4a1410d45a4 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -641,12 +641,12 @@ async def test_statistic_during_period_hole( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test statistic_during_period when there are holes in the data.""" - id = 1 + stat_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal stat_id + stat_id += 1 + return stat_id now = dt_util.utcnow() diff --git a/tests/components/refoss/__init__.py b/tests/components/refoss/__init__.py index e7c160ef0af..51c4261b954 100644 --- a/tests/components/refoss/__init__.py +++ b/tests/components/refoss/__init__.py @@ -62,7 +62,7 @@ class FakeDiscovery: def build_device_mock(name="r10", ip="1.1.1.1", mac="aabbcc112233"): """Build mock device object.""" - mock = Mock( + return Mock( uuid="abc", dev_name=name, device_type="r10", @@ -74,7 +74,6 @@ def build_device_mock(name="r10", ip="1.1.1.1", mac="aabbcc112233"): sub_type="eu", channels=[0], ) - return mock def build_base_device_mock(name="r10", ip="1.1.1.1", mac="aabbcc112233"): diff --git a/tests/components/refoss/test_config_flow.py b/tests/components/refoss/test_config_flow.py index f022c950635..ae40f6dd8b2 100644 --- a/tests/components/refoss/test_config_flow.py +++ b/tests/components/refoss/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import AsyncMock, patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.refoss.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import FakeDiscovery, build_base_device_mock @@ -33,11 +34,11 @@ async def test_creating_entry_sets_up( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -60,10 +61,10 @@ async def test_creating_entry_has_no_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 09c68843872..50a859af446 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index e7826f4952c..4fd14e82990 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory @@ -217,8 +217,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -236,8 +238,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -300,8 +304,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -361,9 +367,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index b77d971e9a6..68f7215186f 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory @@ -212,15 +212,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -236,15 +233,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -260,15 +254,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -331,15 +322,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -395,15 +383,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 15fbb1174c6..575e69015fe 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.components.remote import ( ATTR_ALTERNATIVE, ATTR_COMMAND, diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index eca7991a27c..7d40cf69314 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -7,7 +7,7 @@ from renault_api.gigya.exceptions import InvalidCredentialsException from renault_api.kamereon import schemas from renault_api.renault_account import RenaultAccount -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, @@ -16,6 +16,7 @@ from homeassistant.components.renault.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import aiohttp_client from .const import MOCK_CONFIG @@ -32,7 +33,7 @@ async def test_config_flow_single_account( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # Failed credentials @@ -49,7 +50,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_credentials"} renault_account = AsyncMock() @@ -80,7 +81,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_1" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -97,7 +98,7 @@ async def test_config_flow_no_account( 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["type"] is FlowResultType.FORM assert result["errors"] == {} # Account list empty @@ -117,7 +118,7 @@ async def test_config_flow_no_account( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "kamereon_no_account" assert len(mock_setup_entry.mock_calls) == 0 @@ -130,7 +131,7 @@ async def test_config_flow_multiple_accounts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} renault_account_1 = RenaultAccount( @@ -160,7 +161,7 @@ async def test_config_flow_multiple_accounts( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "kamereon" # Account selected @@ -168,7 +169,7 @@ async def test_config_flow_multiple_accounts( result["flow_id"], user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_2" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -188,7 +189,7 @@ async def test_config_flow_duplicate( 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["type"] is FlowResultType.FORM assert result["errors"] == {} renault_account = RenaultAccount( @@ -212,7 +213,7 @@ async def test_config_flow_duplicate( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -233,7 +234,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry) -> None: data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result["errors"] == {} @@ -247,7 +248,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry) -> None: user_input={CONF_PASSWORD: "any"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result2["errors"] == {"base": "invalid_credentials"} @@ -258,5 +259,5 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry) -> None: user_input={CONF_PASSWORD: "any"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/renson/test_config_flow.py b/tests/components/renson/test_config_flow.py index 6d51605824e..2f60149d14d 100644 --- a/tests/components/renson/test_config_flow.py +++ b/tests/components/renson/test_config_flow.py @@ -13,7 +13,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Renson" assert result2["data"] == { "host": "1.1.1.1", @@ -59,7 +59,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -80,5 +80,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index e8818c9e560..de1e7a0bc83 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, call import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac from homeassistant.util.dt import utcnow @@ -53,7 +54,7 @@ async def test_config_flow_manual_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +67,7 @@ async def test_config_flow_manual_success( }, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NVR_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -88,7 +89,7 @@ async def test_config_flow_errors( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -103,7 +104,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_USERNAME: "not_admin"} @@ -119,7 +120,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -133,7 +134,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "webhook_exception"} @@ -149,7 +150,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "unknown"} @@ -163,7 +164,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "invalid_auth"} @@ -177,7 +178,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "api_error"} @@ -193,7 +194,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NVR_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -231,7 +232,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -239,7 +240,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> user_input={CONF_PROTOCOL: "rtmp"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_PROTOCOL: "rtmp", } @@ -270,7 +271,7 @@ async def test_change_connection_settings( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -283,7 +284,7 @@ async def test_change_connection_settings( }, ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 @@ -323,7 +324,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: data=config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -331,7 +332,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: {}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -343,7 +344,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == TEST_HOST assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 @@ -362,7 +363,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -374,7 +375,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No }, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NVR_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -396,7 +397,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No None, None, TEST_HOST2, - [TEST_HOST, TEST_HOST2], + [TEST_HOST, TEST_HOST2, TEST_HOST2], ), ( True, @@ -451,7 +452,7 @@ async def test_dhcp_ip_update( assert 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 is ConfigEntryState.LOADED if not last_update_success: # ensure the last_update_succes is False for the device_coordinator. @@ -489,7 +490,7 @@ async def test_dhcp_ip_update( assert reolink_connect_class.call_args_list == expected_calls - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 8ebce5d350e..4ec02244c91 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -68,7 +68,7 @@ async def test_failures_parametrized( """Test outcomes when changing errors.""" setattr(reolink_connect, attr, value) assert await hass.config_entries.async_setup(config_entry.entry_id) is ( - expected == ConfigEntryState.LOADED + expected is ConfigEntryState.LOADED ) await hass.async_block_till_done() @@ -88,7 +88,7 @@ async def test_firmware_error_twice( assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware" assert hass.states.is_state(entity_id, STATE_OFF) diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index ec34409eb74..75088f6c370 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock +from awesomeversion.exceptions import AwesomeVersionStrategyException from freezegun.api import FrozenDateTimeFactory import pytest @@ -145,7 +146,7 @@ async def test_create_issue_invalid_version( "translation_placeholders": {"abc": "123"}, } - with pytest.raises(Exception): + with pytest.raises(AwesomeVersionStrategyException): async_create_issue( hass, issue["domain"], diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 38a1661a831..0fda89cc329 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -475,3 +475,62 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None: assert len(config["rest"]) == 2 assert config["rest"][0]["resource"] == "http://url1" assert config["rest"][1]["resource"] == "http://url2" + + +@respx.mock +async def test_setup_minimum_payload_template(hass: HomeAssistant) -> None: + """Test setup with minimum configuration (payload_template).""" + + respx.post("http://localhost", json={"data": "value"}).respond( + status_code=HTTPStatus.OK, + json={ + "sensor1": "1", + "sensor2": "2", + "binary_sensor1": "on", + "binary_sensor2": "off", + }, + ) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "http://localhost", + "payload_template": '{% set payload = {"data": "value"} %}{{ payload | to_json }}', + "method": "POST", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "unit_of_measurement": UnitOfInformation.MEGABYTES, + "name": "sensor1", + "value_template": "{{ value_json.sensor1 }}", + }, + { + "unit_of_measurement": UnitOfInformation.MEGABYTES, + "name": "sensor2", + "value_template": "{{ value_json.sensor2 }}", + }, + ], + "binary_sensor": [ + { + "name": "binary_sensor1", + "value_template": "{{ value_json.binary_sensor1 }}", + }, + { + "name": "binary_sensor2", + "value_template": "{{ value_json.binary_sensor2 }}", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + assert hass.states.get("sensor.sensor1").state == "1" + assert hass.states.get("sensor.sensor2").state == "2" + assert hass.states.get("binary_sensor.binary_sensor1").state == "on" + assert hass.states.get("binary_sensor.binary_sensor2").state == "off" diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index 9f47e74c535..9731388a26e 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -5,7 +5,7 @@ from unittest.mock import patch import respx from homeassistant import config as hass_config -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 567391a4b32..4f88e1b9d34 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -68,7 +68,7 @@ async def test_rest_command_timeout( 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 str(exc.value) == 'Timeout when calling resource "https://example.com/"' assert len(aioclient_mock.mock_calls) == 1 @@ -88,7 +88,7 @@ async def test_rest_command_aiohttp_error( assert ( str(exc.value) - == "Client error occurred when calling resource 'https://example.com/'" + == 'Client error occurred when calling resource "https://example.com/"' ) assert len(aioclient_mock.mock_calls) == 1 @@ -341,7 +341,7 @@ async def test_rest_command_get_response_malformed_json( ) assert ( str(exc.value) - == "The response of 'https://example.com/' could not be decoded as JSON" + == 'The response of "https://example.com/" could not be decoded as JSON' ) @@ -375,7 +375,7 @@ async def test_rest_command_get_response_none( ) assert ( str(exc.value) - == "The response of 'https://example.com/' could not be decoded as text" + == 'The response of "https://example.com/" could not be decoded as text' ) assert not response diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 67f6aa5e6f6..8f09c4a2e54 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -57,8 +57,7 @@ async def mock_rflink( if fail: raise ConnectionRefusedError - else: - return transport, protocol + return transport, protocol mock_create = Mock(wraps=create_rflink_connection) monkeypatch.setattr( diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index d3c87885782..3e97b4cfc30 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import MagicMock, patch, sentinel from RFXtrx import RFXtrxTransportError import serial.tools.list_ports -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN, config_flow from homeassistant.const import STATE_UNKNOWN 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 @@ -44,7 +45,7 @@ async def test_setup_network(transport_mock, hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -53,7 +54,7 @@ async def test_setup_network(transport_mock, hass: HomeAssistant) -> None: {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -62,7 +63,7 @@ async def test_setup_network(transport_mock, hass: HomeAssistant) -> None: result["flow_id"], {"host": "10.10.0.1", "port": 1234} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "RFXTRX" assert result["data"] == { "host": "10.10.0.1", @@ -82,7 +83,7 @@ async def test_setup_serial(com_mock, transport_mock, hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -91,7 +92,7 @@ async def test_setup_serial(com_mock, transport_mock, hass: HomeAssistant) -> No {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -100,7 +101,7 @@ async def test_setup_serial(com_mock, transport_mock, hass: HomeAssistant) -> No result["flow_id"], {"device": port.device} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "RFXTRX" assert result["data"] == { "host": None, @@ -120,7 +121,7 @@ async def test_setup_serial_manual( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -129,7 +130,7 @@ async def test_setup_serial_manual( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -137,7 +138,7 @@ async def test_setup_serial_manual( result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -146,7 +147,7 @@ async def test_setup_serial_manual( result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "RFXTRX" assert result["data"] == { "host": None, @@ -164,7 +165,7 @@ async def test_setup_network_fail(transport_mock, hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -173,7 +174,7 @@ async def test_setup_network_fail(transport_mock, hass: HomeAssistant) -> None: {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -181,7 +182,7 @@ async def test_setup_network_fail(transport_mock, hass: HomeAssistant) -> None: result["flow_id"], {"host": "10.10.0.1", "port": 1234} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {"base": "cannot_connect"} @@ -196,7 +197,7 @@ async def test_setup_serial_fail(com_mock, transport_mock, hass: HomeAssistant) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -205,7 +206,7 @@ async def test_setup_serial_fail(com_mock, transport_mock, hass: HomeAssistant) {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -213,7 +214,7 @@ async def test_setup_serial_fail(com_mock, transport_mock, hass: HomeAssistant) result["flow_id"], {"device": port.device} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_connect"} @@ -228,7 +229,7 @@ async def test_setup_serial_manual_fail( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -237,7 +238,7 @@ async def test_setup_serial_manual_fail( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -245,7 +246,7 @@ async def test_setup_serial_manual_fail( result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -253,7 +254,7 @@ async def test_setup_serial_manual_fail( result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {"base": "cannot_connect"} @@ -276,7 +277,7 @@ async def test_options_global(hass: HomeAssistant) -> None: with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -284,7 +285,7 @@ async def test_options_global(hass: HomeAssistant) -> None: user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -311,7 +312,7 @@ async def test_no_protocols(hass: HomeAssistant) -> None: with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -319,7 +320,7 @@ async def test_no_protocols(hass: HomeAssistant) -> None: user_input={"automatic_add": False, "protocols": []}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -344,7 +345,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None: ) result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" # Try with invalid event code @@ -353,7 +354,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None: user_input={"automatic_add": True, "event_code": "1234"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" assert result["errors"] assert result["errors"]["event_code"] == "invalid_event_code" @@ -367,14 +368,14 @@ async def test_options_add_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -408,7 +409,7 @@ async def test_options_add_duplicate_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -419,7 +420,7 @@ async def test_options_add_duplicate_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" assert result["errors"] assert result["errors"]["event_code"] == "already_configured_device" @@ -507,7 +508,7 @@ async def test_options_replace_sensor_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -518,7 +519,7 @@ async def test_options_replace_sensor_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -528,7 +529,7 @@ async def test_options_replace_sensor_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -640,7 +641,7 @@ async def test_options_replace_control_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -651,7 +652,7 @@ async def test_options_replace_control_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -661,7 +662,7 @@ async def test_options_replace_control_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -701,7 +702,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: ) result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -712,7 +713,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -725,7 +726,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" assert result["errors"] assert result["errors"]["off_delay"] == "invalid_input_off_delay" @@ -742,7 +743,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -763,7 +764,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -774,7 +775,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -786,7 +787,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -810,7 +811,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: ) result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -821,7 +822,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -848,7 +849,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -859,7 +860,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -869,7 +870,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index a717fcf35d6..c678f2dfc62 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -8,7 +8,7 @@ import pytest from pytest_unordered import unordered import RFXtrx -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 7d24ec3ff6a..629ff897eb7 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -7,7 +7,7 @@ from typing import Any, NamedTuple import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/rfxtrx/test_event.py b/tests/components/rfxtrx/test_event.py index 1a4305d97f6..035949efe3b 100644 --- a/tests/components/rfxtrx/test_event.py +++ b/tests/components/rfxtrx/test_event.py @@ -10,6 +10,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.rfxtrx import get_rfx_object from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import setup_rfx_test_cfg @@ -101,3 +102,25 @@ async def test_invalid_event_type( await hass.async_block_till_done() assert hass.states.get("event.arc_c1") == state + + +async def test_ignoring_lighting4(hass: HomeAssistant, rfxtrx) -> None: + """Test with 1 sensor.""" + entry = await setup_rfx_test_cfg( + hass, + devices={ + "0913000022670e013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + }, + ) + + registry = er.async_get(hass) + entries = [ + entry + for entry in registry.entities.get_entries_for_config_entry_id(entry.entry_id) + if entry.domain == Platform.EVENT + ] + assert entries == [] diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 63aacdd5eab..7acc008cc8a 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -4,8 +4,8 @@ from unittest.mock import call import pytest -from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant, State @@ -286,4 +286,4 @@ async def test_unknown_event_code(hass: HomeAssistant, rfxtrx) -> None: assert len(conf_entries) == 1 entry = conf_entries[0] - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/rhasspy/test_config_flow.py b/tests/components/rhasspy/test_config_flow.py index 1a53dd32e04..7f74143c67c 100644 --- a/tests/components/rhasspy/test_config_flow.py +++ b/tests/components/rhasspy/test_config_flow.py @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Rhasspy" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -41,5 +41,5 @@ async def test_single_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 15352929b4c..601ac182670 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -28,7 +28,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise: @@ -39,7 +39,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -47,7 +47,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -60,7 +60,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,6 +75,6 @@ async def test_step_reauth( result["flow_id"], user_input={CONF_PASSWORD: "new_password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index c6852bf87d6..b129623aa95 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -15,4 +15,4 @@ async def setup_platform(hass, platform): ) with patch("homeassistant.components.ring.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py index 6f0c29b1fcc..6b2200b2bf3 100644 --- a/tests/components/ring/test_button.py +++ b/tests/components/ring/test_button.py @@ -39,4 +39,4 @@ async def test_button_opens_door( ) await hass.async_block_till_done() - assert mock.called_once + assert mock.call_count == 1 diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py index de61d7a1452..dde1252d5b8 100644 --- a/tests/components/ring/test_camera.py +++ b/tests/components/ring/test_camera.py @@ -25,10 +25,10 @@ async def test_entity_registry( entity_registry = er.async_get(hass) entry = entity_registry.async_get("camera.front") - assert entry.unique_id == 765432 + assert entry.unique_id == "765432" entry = entity_registry.async_get("camera.internal") - assert entry.unique_id == 345678 + assert entry.unique_id == "345678" @pytest.mark.parametrize( diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index f9c24ad77c5..bedb4604814 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -24,7 +24,7 @@ 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"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "hello@home-assistant.io" assert result2["data"] == { "username": "hello@home-assistant.io", @@ -63,7 +63,7 @@ async def test_form_error( {"username": "hello@home-assistant.io", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": errors_msg} @@ -76,7 +76,7 @@ async def test_form_2fa( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_ring_auth.fetch_token.side_effect = ring_doorbell.Requires2FAError @@ -92,7 +92,7 @@ async def test_form_2fa( "foo@bar.com", "fake-password", None ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.fetch_token.return_value = "new-foobar" @@ -104,7 +104,7 @@ async def test_form_2fa( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "fake-password", "123456" ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "foo@bar.com" assert result3["data"] == { "username": "foo@bar.com", @@ -139,7 +139,7 @@ async def test_reauth( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "other_fake_password", None ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.fetch_token.return_value = "new-foobar" @@ -151,7 +151,7 @@ async def test_reauth( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "other_fake_password", "123456" ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_added_config_entry.data == { "username": "foo@bar.com", @@ -197,7 +197,7 @@ async def test_reauth_error( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "error_fake_password", None ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": errors_msg} # Now test reauth can go on to succeed @@ -213,7 +213,7 @@ async def test_reauth_error( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "other_fake_password", None ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_added_config_entry.data == { "username": "foo@bar.com", diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index ba5dd03ba9c..664f8ff1973 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -7,10 +7,14 @@ import pytest import requests_mock from ring_doorbell import AuthenticationError, RingError, RingTimeout -import homeassistant.components.ring as ring +from homeassistant.components import ring +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.ring import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.issue_registry import IssueRegistry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -253,7 +257,7 @@ async def test_issue_deprecated_service_ring_update( 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) + await hass.services.async_call(DOMAIN, "update", {}, blocking=True) issue = issue_registry.async_get_issue("ring", "deprecated_service_ring_update") assert issue @@ -266,3 +270,135 @@ async def test_issue_deprecated_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 + + +@pytest.mark.parametrize( + ("domain", "old_unique_id"), + [ + ( + LIGHT_DOMAIN, + 123456, + ), + ( + CAMERA_DOMAIN, + 654321, + ), + ], +) +async def test_update_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, + domain: str, + old_unique_id: int | str, +) -> None: + """Test unique_id update of integration.""" + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == str(old_unique_id) + assert (f"Fixing non string unique id {old_unique_id}") in caplog.text + + +async def test_update_unique_id_existing( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, +) -> None: + """Test unique_id update of integration.""" + old_unique_id = 123456 + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + domain=CAMERA_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + entity_existing = entity_registry.async_get_or_create( + domain=CAMERA_DOMAIN, + platform=DOMAIN, + unique_id=str(old_unique_id), + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + assert entity_existing.unique_id == str(old_unique_id) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_not_migrated = entity_registry.async_get(entity.entity_id) + entity_existing = entity_registry.async_get(entity_existing.entity_id) + assert entity_not_migrated + assert entity_existing + assert entity_not_migrated.unique_id == old_unique_id + assert ( + f"Cannot migrate to unique_id '{old_unique_id}', " + f"already exists for '{entity_existing.entity_id}', " + "You may have to delete unavailable ring entities" + ) in caplog.text + + +async def test_update_unique_id_no_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, +) -> None: + """Test unique_id update of integration.""" + correct_unique_id = "123456" + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + domain=CAMERA_DOMAIN, + platform=DOMAIN, + unique_id="123456", + config_entry=entry, + ) + assert entity.unique_id == correct_unique_id + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == correct_unique_id + assert "Fixing non string unique id" not in caplog.text diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 621c0b8f1d0..ac0f3b70d27 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -25,10 +25,10 @@ async def test_entity_registry( entity_registry = er.async_get(hass) entry = entity_registry.async_get("light.front_light") - assert entry.unique_id == 765432 + assert entry.unique_id == "765432" entry = entity_registry.async_get("light.internal_light") - assert entry.unique_id == 345678 + assert entry.unique_id == "345678" async def test_light_off_reports_correctly( diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index ea18c59e236..b6ea723064e 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -122,11 +122,11 @@ async def test_local_setup( async def _check_local_state( - hass, zones, property, value, entity_id, zone_id, callback + hass, zones, entity_property, value, entity_id, zone_id, callback ): with patch.object( zones[zone_id], - property, + entity_property, new_callable=PropertyMock(return_value=value), ): await callback(zone_id, zones[zone_id]) @@ -210,19 +210,19 @@ async def test_armed_local_states( ) -async def _check_system_state(hass, system, property, value, callback): +async def _check_system_state(hass, system, entity_property, value, callback): with patch.object( system, - property, + entity_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}" + if entity_property == "ac_trouble": + entity_property = "a_c_trouble" + entity_id = f"binary_sensor.test_site_name_{entity_property}" assert hass.states.get(entity_id).state == expected_value @@ -275,6 +275,10 @@ async def test_system_states( "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) + for entity_property in properties: + await _check_system_state( + hass, system_only_local, entity_property, True, callback + ) + await _check_system_state( + hass, system_only_local, entity_property, False, callback + ) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index db39447c69a..9fade18ea96 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -61,13 +61,13 @@ async def test_cloud_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "cloud"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} with ( @@ -92,7 +92,7 @@ async def test_cloud_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_SITE_NAME assert result3["data"] == TEST_CLOUD_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -124,7 +124,7 @@ async def test_cloud_error(hass: HomeAssistant, login_with_error, error) -> None ) mock_close.assert_awaited_once() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": error} @@ -150,7 +150,7 @@ async def test_form_cloud_already_exists(hass: HomeAssistant) -> None: result2["flow_id"], TEST_CLOUD_DATA ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -162,7 +162,7 @@ async def test_form_reauth(hass: HomeAssistant, cloud_config_entry) -> None: context={"source": config_entries.SOURCE_REAUTH}, data=cloud_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -187,7 +187,7 @@ async def test_form_reauth(hass: HomeAssistant, cloud_config_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert cloud_config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 @@ -203,7 +203,7 @@ async def test_form_reauth_with_new_username( context={"source": config_entries.SOURCE_REAUTH}, data=cloud_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -228,7 +228,7 @@ async def test_form_reauth_with_new_username( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert cloud_config_entry.data[CONF_USERNAME] == "new_user" assert cloud_config_entry.unique_id == "new_user" @@ -240,13 +240,13 @@ async def test_local_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "local"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} with ( @@ -276,7 +276,7 @@ async def test_local_form(hass: HomeAssistant) -> None: "type": "local", CONF_COMMUNICATION_DELAY: 0, } - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_SITE_NAME assert result3["data"] == expected_data assert len(mock_setup_entry.mock_calls) == 1 @@ -304,7 +304,7 @@ async def test_local_error(hass: HomeAssistant, connect_with_error, error) -> No result2["flow_id"], TEST_LOCAL_DATA ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": error} @@ -343,7 +343,7 @@ async def test_form_local_already_exists(hass: HomeAssistant) -> None: result2["flow_id"], TEST_LOCAL_DATA ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -359,14 +359,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=TEST_OPTIONS, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "risco_to_ha" result = await hass.config_entries.options.async_configure( @@ -374,7 +374,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input=TEST_RISCO_TO_HA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ha_to_risco" with patch("homeassistant.components.risco.async_setup_entry", return_value=True): @@ -383,7 +383,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { **TEST_OPTIONS, "risco_states_to_ha": TEST_RISCO_TO_HA, diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 157eb3e62b5..a8236ad3d87 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -133,8 +133,8 @@ async def test_error_on_login( await hass.async_block_till_done() registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert not registry.async_is_registered(id) + for entity_id in ENTITY_IDS.values(): + assert not registry.async_is_registered(entity_id) def _check_state(hass, category, entity_id): @@ -184,8 +184,8 @@ async def test_cloud_setup( ) -> None: """Test entity setup.""" registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert registry.async_is_registered(id) + for entity_id in ENTITY_IDS.values(): + assert registry.async_is_registered(entity_id) save_mock.assert_awaited_once_with({LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}) for category, entity_id in ENTITY_IDS.items(): @@ -213,5 +213,5 @@ async def test_local_setup( ) -> None: """Test entity setup.""" registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert not registry.async_is_registered(id) + for entity_id in ENTITY_IDS.values(): + assert not registry.async_is_registered(entity_id) diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index 45f14399f15..6c0a09a8303 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.rituals_perfume_genie.const import ACCOUNT_HASH, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_EMAIL = "rituals@example.com" VALID_PASSWORD = "passw0rd" @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_EMAIL assert isinstance(result2["data"][ACCOUNT_HASH], str) assert len(mock_setup_entry.mock_calls) == 1 @@ -75,7 +76,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +98,7 @@ async def test_form_auth_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -121,5 +122,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/rituals_perfume_genie/test_number.py b/tests/components/rituals_perfume_genie/test_number.py index f88bcc6d0cb..ddca70649b5 100644 --- a/tests/components/rituals_perfume_genie/test_number.py +++ b/tests/components/rituals_perfume_genie/test_number.py @@ -14,6 +14,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -86,7 +87,7 @@ async def test_set_number_value_out_of_range(hass: HomeAssistant) -> None: assert state assert state.state == "2" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -105,7 +106,7 @@ async def test_set_number_value_out_of_range(hass: HomeAssistant) -> None: assert state assert state.state == "2" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 91331a1486a..d3bb0a221b1 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from roborock import RoomMapping from homeassistant.components.roborock.const import ( CONF_BASE_URL, @@ -31,26 +32,26 @@ from tests.common import MockConfigEntry 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.RoborockMqttClientV1.async_connect"), + patch("homeassistant.components.roborock.RoborockMqttClientV1._send_command"), patch( "homeassistant.components.roborock.RoborockApiClient.get_home_data", return_value=HOME_DATA, ), patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", + "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", return_value=NETWORK_INFO, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=PROP, ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_multi_maps_list", + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_multi_maps_list", return_value=MULTI_MAP_LIST, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", return_value=MULTI_MAP_LIST, ), patch( @@ -58,22 +59,42 @@ def bypass_api_fixture() -> None: return_value=MAP_DATA, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ), - patch("homeassistant.components.roborock.RoborockMqttClient._wait_response"), + patch("homeassistant.components.roborock.RoborockMqttClientV1._wait_response"), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._wait_response" ), patch( - "roborock.api.AttributeCache.async_value", + "roborock.version_1_apis.AttributeCache.async_value", ), patch( - "roborock.api.AttributeCache.value", + "roborock.version_1_apis.AttributeCache.value", ), patch( "homeassistant.components.roborock.image.MAP_SLEEP", 0, ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_room_mapping", + return_value=[ + RoomMapping(16, "2362048"), + RoomMapping(17, "2362044"), + RoomMapping(18, "2362041"), + ], + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_room_mapping", + return_value=[ + RoomMapping(16, "2362048"), + RoomMapping(17, "2362044"), + RoomMapping(18, "2362041"), + ], + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=b"123", + ), ): yield diff --git a/tests/components/roborock/snapshots/test_vacuum.ambr b/tests/components/roborock/snapshots/test_vacuum.ambr new file mode 100644 index 00000000000..d03bec28125 --- /dev/null +++ b/tests/components/roborock/snapshots/test_vacuum.ambr @@ -0,0 +1,27 @@ +# serializer version: 1 +# name: test_get_maps + dict({ + 'vacuum.roborock_s7_maxv': dict({ + 'maps': list([ + dict({ + 'flag': 0, + 'name': 'Upstairs', + 'rooms': dict({ + 16: 'Example room 1', + 17: 'Example room 2', + 18: 'Example room 3', + }), + }), + dict({ + 'flag': 1, + 'name': 'Downstairs', + 'rooms': dict({ + 16: 'Example room 1', + 17: 'Example room 2', + 18: 'Example room 3', + }), + }), + ]), + }), + }) +# --- diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py index 5654dac9218..88cf5beab15 100644 --- a/tests/components/roborock/test_button.py +++ b/tests/components/roborock/test_button.py @@ -31,7 +31,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id).state == "unknown" with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "button", diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index b5cff60cddb..fc097dd73ae 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" @@ -42,7 +42,7 @@ async def test_config_flow_success( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} with patch( @@ -53,7 +53,7 @@ async def test_config_flow_success( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -86,7 +86,7 @@ async def test_config_flow_failures_request_code( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code", @@ -95,7 +95,7 @@ async def test_config_flow_failures_request_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == request_code_errors # Recover from error with patch( @@ -105,7 +105,7 @@ async def test_config_flow_failures_request_code( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} with patch( @@ -116,7 +116,7 @@ async def test_config_flow_failures_request_code( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -147,7 +147,7 @@ async def test_config_flow_failures_code_login( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" @@ -156,7 +156,7 @@ async def test_config_flow_failures_code_login( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} # Raise exception for invalid code @@ -167,7 +167,7 @@ async def test_config_flow_failures_code_login( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == code_login_errors with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.code_login", @@ -177,7 +177,7 @@ async def test_config_flow_failures_code_login( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -205,7 +205,7 @@ async def test_reauth_flow( ) # Enter a new code assert result["step_id"] == "code" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM new_user_data = deepcopy(USER_DATA) new_user_data.rriot.s = "new_password_hash" with patch( @@ -215,6 +215,6 @@ async def test_reauth_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_roborock_entry.data["user_data"]["rriot"]["s"] == "new_password_hash" diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index 77829e5aaa6..bc45c6dec05 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -5,7 +5,12 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +from roborock import RoborockException + +from homeassistant.components.roborock import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -37,7 +42,7 @@ async def test_floorplan_image( prop.status.in_cleaning = 1 with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, ), patch( @@ -72,7 +77,7 @@ async def test_floorplan_image_failed_parse( return_value=map_data, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, ), patch( @@ -82,3 +87,60 @@ async def test_floorplan_image_failed_parse( async_fire_time_changed(hass, now) resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") assert not resp.ok + + +async def test_fail_parse_on_startup( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture, +) -> None: + """Test that if we fail parsing on startup, we create the entity but set it as unavailable.""" + map_data = copy.deepcopy(MAP_DATA) + map_data.image = None + with patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert ( + image_entity := hass.states.get("image.roborock_s7_maxv_upstairs") + ) is not None + assert image_entity.state == STATE_UNAVAILABLE + + +async def test_fail_updating_image( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test that we handle failing getting the image after it has already been setup..""" + client = await hass_client() + map_data = copy.deepcopy(MAP_DATA) + map_data.image = None + now = dt_util.utcnow() + timedelta(seconds=91) + # Copy the device prop so we don't override it + 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.RoborockLocalClientV1.get_prop", + return_value=prop, + ), + patch( + "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + side_effect=RoborockException, + ), + ): + async_fire_time_changed(hass, now) + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + assert not resp.ok diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 08a3afe6c5e..de858ef7cb2 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -19,7 +19,7 @@ async def test_unload_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert setup_entry.state is ConfigEntryState.LOADED with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.async_release" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.async_release" ) as mock_disconnect: assert await hass.config_entries.async_unload(setup_entry.entry_id) await hass.async_block_till_done() @@ -37,7 +37,7 @@ async def test_config_entry_not_ready( "homeassistant.components.roborock.RoborockApiClient.get_home_data", ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", side_effect=RoborockException(), ), ): @@ -55,7 +55,7 @@ async def test_config_entry_not_ready_home_data( side_effect=RoborockException(), ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", side_effect=RoborockException(), ), ): @@ -68,7 +68,7 @@ async def test_get_networking_fails( ) -> None: """Test that when networking fails, we attempt to retry.""" with patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", + "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) @@ -80,7 +80,7 @@ async def test_get_networking_fails_none( ) -> None: """Test that when networking returns None, we attempt to retry.""" with patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", + "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", return_value=None, ): await async_setup_component(hass, DOMAIN, {}) @@ -93,11 +93,11 @@ async def test_cloud_client_fails_props( """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", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.ping", side_effect=RoborockException(), ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_prop", side_effect=RoborockException(), ), ): @@ -110,7 +110,7 @@ async def test_local_client_fails_props( ) -> None: """Test that if networking succeeds, but we can't communicate locally with the vacuum, we can't get props, fail.""" with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) @@ -122,7 +122,7 @@ async def test_fails_maps_continue( ) -> None: """Test that if we fail to get the maps, we still setup.""" with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py index 1c20a93cace..3291dd2a7dc 100644 --- a/tests/components/roborock/test_number.py +++ b/tests/components/roborock/test_number.py @@ -27,7 +27,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "number", diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 9310d4e2e9a..c8626818749 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -30,7 +30,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "select", @@ -50,7 +50,7 @@ async def test_update_failure( """Test that changing a value will raise a homeassistanterror when it fails.""" with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message", side_effect=RoborockException(), ), pytest.raises(HomeAssistantError), diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index a5f4164eee1..88ed6e1098c 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -3,7 +3,6 @@ from unittest.mock import patch from roborock import DeviceData, HomeDataDevice -from roborock.cloud_api import RoborockMqttClient from roborock.const import ( FILTER_REPLACE_TIME, MAIN_BRUSH_REPLACE_TIME, @@ -11,6 +10,7 @@ from roborock.const import ( SIDE_BRUSH_REPLACE_TIME, ) from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol +from roborock.version_1_apis import RoborockMqttClientV1 from homeassistant.core import HomeAssistant @@ -62,11 +62,11 @@ async def test_listener_update( """Test that when we receive a mqtt topic, we successfully update the entity.""" assert hass.states.get("sensor.roborock_s7_maxv_status").state == "charging" # Listeners are global based on uuid - so this is okay - client = RoborockMqttClient( + client = RoborockMqttClientV1( USER_DATA, DeviceData(device=HomeDataDevice("abc123", "", "", "", ""), model="") ) # Test Status - with patch("roborock.api.AttributeCache.value", STATUS.as_dict()): + with patch("roborock.version_1_apis.AttributeCache.value", STATUS.as_dict()): # Symbolizes a mqtt message coming in client.on_message_received( [ @@ -80,7 +80,7 @@ async def test_listener_update( assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( FILTER_REPLACE_TIME - 74382 ) - with patch("roborock.api.AttributeCache.value", CONSUMABLE.as_dict()): + with patch("roborock.version_1_apis.AttributeCache.value", CONSUMABLE.as_dict()): client.on_message_received( [ RoborockMessage( @@ -89,6 +89,7 @@ async def test_listener_update( ) ] ) + await hass.async_block_till_done() assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( FILTER_REPLACE_TIME - 743 ) diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 42a5e92f32a..3afa72b319d 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -28,7 +28,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command" ) as mock_send_message: await hass.services.async_call( "switch", @@ -39,7 +39,7 @@ async def test_update_success( ) assert mock_send_message.assert_called_once with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "switch", diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 378c642b2f4..ca6507f887b 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -28,7 +28,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command" ) as mock_send_message: await hass.services.async_call( "time", diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index a3d5854edd1..437c9847e21 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -7,8 +7,10 @@ from unittest.mock import patch import pytest from roborock import RoborockException from roborock.roborock_typing import RoborockCommand +from syrupy.assertion import SnapshotAssertion from homeassistant.components.roborock import DOMAIN +from homeassistant.components.roborock.const import GET_MAPS_SERVICE_NAME from homeassistant.components.vacuum import ( SERVICE_CLEAN_SPOT, SERVICE_LOCATE, @@ -80,7 +82,7 @@ async def test_commands( data = {ATTR_ENTITY_ID: ENTITY_ID, **(service_params or {})} with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command" ) as mock_send_command: await hass.services.async_call( Platform.VACUUM, @@ -113,7 +115,7 @@ async def test_resume_cleaning( prop = copy.deepcopy(PROP) prop.status.in_cleaning = in_cleaning_int with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, ): await async_setup_component(hass, DOMAIN, {}) @@ -122,7 +124,7 @@ async def test_resume_cleaning( data = {ATTR_ENTITY_ID: ENTITY_ID} with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command" ) as mock_send_command: await hass.services.async_call( Platform.VACUUM, @@ -143,7 +145,7 @@ async def test_failed_user_command( data = {ATTR_ENTITY_ID: ENTITY_ID, "command": "fake_command"} with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command", side_effect=RoborockException(), ), pytest.raises(HomeAssistantError, match="Error while calling fake_command"), @@ -154,3 +156,20 @@ async def test_failed_user_command( data, blocking=True, ) + + +async def test_get_maps( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test that the service for maps correctly outputs rooms with the right name.""" + response = await hass.services.async_call( + DOMAIN, + GET_MAPS_SERVICE_NAME, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + return_response=True, + ) + assert response == snapshot diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 34640474bcd..3cf5627f342 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -37,7 +37,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} @@ -45,7 +45,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) @@ -53,7 +53,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +66,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} user_input = {CONF_HOST: HOST} @@ -75,7 +75,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My Roku 3" assert "data" in result @@ -99,7 +99,7 @@ async def test_form_cannot_connect( flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -118,7 +118,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -135,7 +135,7 @@ async def test_homekit_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -152,7 +152,7 @@ async def test_homekit_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -168,7 +168,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} @@ -177,7 +177,7 @@ async def test_homekit_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME_ROKUTV assert "data" in result @@ -190,7 +190,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -207,7 +207,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -224,7 +224,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -239,7 +239,7 @@ async def test_ssdp_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} @@ -248,7 +248,7 @@ async def test_ssdp_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] diff --git a/tests/components/romy/test_config_flow.py b/tests/components/romy/test_config_flow.py index 480a37fa068..a29f899ee9d 100644 --- a/tests/components/romy/test_config_flow.py +++ b/tests/components/romy/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import Mock, PropertyMock, patch from romy import RomyRobot -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.romy.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def _create_mocked_romy( @@ -56,7 +57,7 @@ async def test_show_user_form_robot_is_offline_and_locked(hass: HomeAssistant) - assert result1["errors"].get("host") == "cannot_connect" assert result1["step_id"] == "user" - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM # Robot is locked with patch( @@ -68,7 +69,7 @@ async def test_show_user_form_robot_is_offline_and_locked(hass: HomeAssistant) - ) assert result2["step_id"] == "password" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM # Robot is initialized and unlocked with patch( @@ -80,7 +81,7 @@ async def test_show_user_form_robot_is_offline_and_locked(hass: HomeAssistant) - ) assert "errors" not in result3 - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> None: @@ -106,7 +107,7 @@ async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "password" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.romy.config_flow.romy.create_romy", @@ -118,7 +119,7 @@ async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> assert result3["errors"] == {"password": "cannot_connect"} assert result3["step_id"] == "password" - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM with patch( "homeassistant.components.romy.config_flow.romy.create_romy", @@ -129,7 +130,7 @@ async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> ) assert "errors" not in result4 - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None: @@ -148,7 +149,7 @@ async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None assert result1["errors"].get("host") == "cannot_connect" assert result1["step_id"] == "user" - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM # Robot is locked with patch( @@ -160,7 +161,7 @@ async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None ) assert "errors" not in result2 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( @@ -188,7 +189,7 @@ async def test_zero_conf_locked_interface_robot(hass: HomeAssistant) -> None: ) assert result1["step_id"] == "password" - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM with patch( "homeassistant.components.romy.config_flow.romy.create_romy", @@ -199,7 +200,7 @@ async def test_zero_conf_locked_interface_robot(hass: HomeAssistant) -> None: ) assert "errors" not in result2 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_zero_conf_uninitialized_robot(hass: HomeAssistant) -> None: @@ -216,7 +217,7 @@ async def test_zero_conf_uninitialized_robot(hass: HomeAssistant) -> None: ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: @@ -233,7 +234,7 @@ async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -246,4 +247,4 @@ async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: assert result["result"] assert result["result"].unique_id == "aicu-aicgsbksisfapcjqmqjq" - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 282884c0be3..e5f882afa36 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -6,12 +6,18 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from roombapy import RoombaConnectionError, RoombaInfo -from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.components.roomba import config_flow from homeassistant.components.roomba.const import CONF_BLID, CONF_CONTINUOUS, DOMAIN +from homeassistant.config_entries import ( + SOURCE_DHCP, + SOURCE_IGNORE, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -20,7 +26,7 @@ VALID_CONFIG = {CONF_HOST: MOCK_IP, CONF_BLID: "BLID", CONF_PASSWORD: "password" DISCOVERY_DEVICES = [ ( - config_entries.SOURCE_DHCP, + SOURCE_DHCP, dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="501479ddeeff", @@ -28,7 +34,7 @@ DISCOVERY_DEVICES = [ ), ), ( - config_entries.SOURCE_DHCP, + SOURCE_DHCP, dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="80a589ddeeff", @@ -36,7 +42,7 @@ DISCOVERY_DEVICES = [ ), ), ( - config_entries.SOURCE_ZEROCONF, + SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( ip_address=ip_address(MOCK_IP), ip_addresses=[ip_address(MOCK_IP)], @@ -48,7 +54,7 @@ DISCOVERY_DEVICES = [ ), ), ( - config_entries.SOURCE_ZEROCONF, + SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( ip_address=ip_address(MOCK_IP), ip_addresses=[ip_address(MOCK_IP)], @@ -157,11 +163,11 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -170,7 +176,7 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -194,7 +200,7 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -217,11 +223,11 @@ async def test_form_user_discovery_skips_known(hass: HomeAssistant) -> None: "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -239,11 +245,11 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured( _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -252,7 +258,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -270,11 +276,11 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -283,7 +289,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -296,7 +302,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] is None with ( @@ -319,7 +325,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -345,11 +351,11 @@ async def test_form_user_discover_fails_aborts_already_configured( _mocked_failed_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -358,7 +364,7 @@ async def test_form_user_discover_fails_aborts_already_configured( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -371,11 +377,11 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -384,7 +390,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -398,7 +404,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -417,11 +423,11 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -433,7 +439,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with ( @@ -456,7 +462,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -484,11 +490,11 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -500,7 +506,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with patch( @@ -529,7 +535,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -558,11 +564,11 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -574,7 +580,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with patch( @@ -603,7 +609,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -622,11 +628,11 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -635,7 +641,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -665,7 +671,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -701,7 +707,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" assert result["description_placeholders"] == {"name": "robot_name"} @@ -726,7 +732,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "robot_name" assert result2["result"].unique_id == "BLID" assert result2["data"] == { @@ -755,12 +761,12 @@ async def test_dhcp_discovery_falls_back_to_manual( ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=discovery_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -769,7 +775,7 @@ async def test_dhcp_discovery_falls_back_to_manual( {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -781,7 +787,7 @@ async def test_dhcp_discovery_falls_back_to_manual( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] is None with ( @@ -804,7 +810,7 @@ async def test_dhcp_discovery_falls_back_to_manual( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -834,12 +840,12 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=discovery_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -851,7 +857,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with ( @@ -874,7 +880,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -890,9 +896,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: """Test ignored entries do not break checking for existing entries.""" - config_entry = MockConfigEntry( - domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE - ) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, source=SOURCE_IGNORE) config_entry.add_to_hass(hass) with patch( @@ -900,7 +904,7 @@ async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -909,7 +913,7 @@ async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> None: @@ -923,7 +927,7 @@ async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> No ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -932,7 +936,7 @@ async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -949,7 +953,7 @@ async def test_dhcp_discovery_already_configured_blid(hass: HomeAssistant) -> No ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -958,7 +962,7 @@ async def test_dhcp_discovery_already_configured_blid(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -975,7 +979,7 @@ async def test_dhcp_discovery_not_irobot(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -984,7 +988,7 @@ async def test_dhcp_discovery_not_irobot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_irobot_device" @@ -996,7 +1000,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -1005,7 +1009,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -1013,7 +1017,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -1022,7 +1026,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" current_flows = hass.config_entries.flow.async_progress() @@ -1034,7 +1038,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ): result3 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -1043,9 +1047,45 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "short_blid" current_flows = hass.config_entries.flow.async_progress() assert len(current_flows) == 1 assert current_flows[0]["flow_id"] == result2["flow_id"] + + +async def test_options_flow( + hass: HomeAssistant, +) -> None: + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="BLID", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_CONTINUOUS: True, CONF_DELAY: 1}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_CONTINUOUS: True, CONF_DELAY: 1} + assert config_entry.options == {CONF_CONTINUOUS: True, CONF_DELAY: 1} diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index c31e689e05b..6f83331d1c7 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.roon.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -93,7 +94,7 @@ async def test_successful_discovery_and_auth(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should go straight to link if server was discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -135,7 +136,7 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass: HomeAssistant) -> await hass.async_block_till_done() # Should show the form if server was not discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "fallback" assert result["errors"] == {} @@ -182,7 +183,7 @@ async def test_duplicate_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should show the form if server was not discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "fallback" assert result["errors"] == {} @@ -194,7 +195,7 @@ async def test_duplicate_config(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -229,7 +230,7 @@ async def test_successful_discovery_no_auth(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should go straight to link if server was discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -264,7 +265,7 @@ async def test_unexpected_exception(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should go straight to link if server was discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} diff --git a/tests/components/rova/test_config_flow.py b/tests/components/rova/test_config_flow.py index 357cd9eb344..d9d1df3e188 100644 --- a/tests/components/rova/test_config_flow.py +++ b/tests/components/rova/test_config_flow.py @@ -5,7 +5,6 @@ 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, @@ -14,6 +13,7 @@ from homeassistant.components.rova.const import ( ) 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 @@ -27,7 +27,7 @@ async def test_user(hass: HomeAssistant, mock_rova: MagicMock) -> None: 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("type") is FlowResultType.FORM assert result.get("step_id") == "user" # test with all information provided @@ -40,7 +40,7 @@ async def test_user(hass: HomeAssistant, mock_rova: MagicMock) -> None: CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, }, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY data = result.get("data") assert data @@ -69,7 +69,7 @@ async def test_error_if_not_rova_area( }, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "invalid_rova_area"} # now reset the return value and test if we can recover @@ -84,7 +84,7 @@ async def test_error_if_not_rova_area( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" assert result["data"] == { CONF_ZIP_CODE: ZIP_CODE, @@ -114,7 +114,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -145,7 +145,7 @@ async def test_abort_if_api_throws_exception( CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, }, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": error} # now reset the side effect to see if we can recover @@ -160,7 +160,7 @@ async def test_abort_if_api_throws_exception( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" assert result["data"] == { CONF_ZIP_CODE: ZIP_CODE, @@ -181,7 +181,7 @@ async def test_import(hass: HomeAssistant, mock_rova: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" assert result["data"] == { CONF_ZIP_CODE: ZIP_CODE, @@ -215,7 +215,7 @@ async def test_import_already_configured( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -237,7 +237,7 @@ async def test_import_if_not_rova_area( }, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "invalid_rova_area" @@ -266,5 +266,5 @@ async def test_import_connection_errors( }, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error diff --git a/tests/components/rova/test_init.py b/tests/components/rova/test_init.py index 3dff0cf4c27..e522d5bfb12 100644 --- a/tests/components/rova/test_init.py +++ b/tests/components/rova/test_init.py @@ -24,12 +24,12 @@ async def test_reload( """Test reloading the integration.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is 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 + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED async def test_service( @@ -68,7 +68,7 @@ async def test_retry_after_failure( 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 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_issue_if_not_rova_area( @@ -83,5 +83,5 @@ async def test_issue_if_not_rova_area( 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 mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert len(issue_registry.issues) == 1 diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py index 78b7b9261b9..1643df6c993 100644 --- a/tests/components/rpi_power/test_binary_sensor.py +++ b/tests/components/rpi_power/test_binary_sensor.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import pytest +from homeassistant.components.rpi_power import binary_sensor from homeassistant.components.rpi_power.binary_sensor import ( DESCRIPTION_NORMALIZED, DESCRIPTION_UNDER_VOLTAGE, @@ -48,32 +49,25 @@ async def test_new_detected( """Test new entry with under voltage detected.""" mocked_under_voltage = await _async_setup_component(hass, True) state = hass.states.get(ENTITY_ID) + assert state assert state.state == STATE_ON assert ( - len( - [ - x - for x in caplog.records - if x.levelno == logging.WARNING - and x.message == DESCRIPTION_UNDER_VOLTAGE - ] - ) - == 1 - ) + binary_sensor.__name__, + logging.WARNING, + DESCRIPTION_UNDER_VOLTAGE, + ) in caplog.record_tuples # back to normal type(mocked_under_voltage).get = MagicMock(return_value=False) future = dt_util.utcnow() + timedelta(minutes=1) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_OFF assert ( - len( - [ - x - for x in caplog.records - if x.levelno == logging.INFO and x.message == DESCRIPTION_NORMALIZED - ] - ) - == 1 - ) + binary_sensor.__name__, + logging.INFO, + DESCRIPTION_NORMALIZED, + ) in caplog.record_tuples diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 1cb9f772d70..1bce51830f0 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -18,13 +18,13 @@ async def test_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert not result["errors"] with patch(MODULE, return_value=MagicMock()): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_not_supported(hass: HomeAssistant) -> None: @@ -36,7 +36,7 @@ async def test_not_supported(hass: HomeAssistant) -> None: with patch(MODULE, return_value=None): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -47,7 +47,7 @@ async def test_onboarding(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_onboarding_not_supported(hass: HomeAssistant) -> None: @@ -57,5 +57,5 @@ async def test_onboarding_not_supported(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index 16d4779a92c..504ede68ac7 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.rtsp_to_webrtc import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ComponentSetup @@ -22,7 +23,7 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") @@ -36,7 +37,7 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "https://example.com" assert "result" in result assert result["result"].data == {"server_url": "https://example.com"} @@ -52,7 +53,7 @@ async def test_single_config_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -61,7 +62,7 @@ async def test_invalid_url(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") @@ -69,7 +70,7 @@ async def test_invalid_url(hass: HomeAssistant) -> None: result["flow_id"], {"server_url": "not-a-url"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"server_url": "invalid_url"} @@ -79,7 +80,7 @@ async def test_server_unreachable(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") with patch( @@ -89,7 +90,7 @@ async def test_server_unreachable(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "server_unreachable"} @@ -99,7 +100,7 @@ async def test_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") with patch( @@ -109,7 +110,7 @@ async def test_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "server_failure"} @@ -130,7 +131,7 @@ async def test_hassio_discovery(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "RTSPtoWebRTC"} @@ -146,7 +147,7 @@ async def test_hassio_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "RTSPtoWebRTC" assert "result" in result assert result["result"].data == {"server_url": "http://fake-server:8083"} @@ -173,7 +174,7 @@ async def test_hassio_single_config_entry(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -196,7 +197,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -217,7 +218,7 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert not result.get("errors") @@ -226,7 +227,7 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: side_effect=rtsp_to_webrtc.exceptions.ResponseError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "server_failure" @@ -246,7 +247,7 @@ async def test_options_flow( assert not config_entry.options result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"stun_server"} @@ -257,17 +258,17 @@ async def test_options_flow( "stun_server": "example.com:1234", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert config_entry.options == {"stun_server": "example.com:1234"} # Clear the value result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert config_entry.options == {} diff --git a/tests/components/rtsp_to_webrtc/test_diagnostics.py b/tests/components/rtsp_to_webrtc/test_diagnostics.py index e020ebfd5f3..ad3522686b6 100644 --- a/tests/components/rtsp_to_webrtc/test_diagnostics.py +++ b/tests/components/rtsp_to_webrtc/test_diagnostics.py @@ -23,8 +23,5 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" await setup_integration() - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "discovery": {"attempt": 1, "web.failure": 1, "webrtc.success": 1}, - "web": {}, - "webrtc": {}, - } + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert "webrtc" in result diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 97b554b1eb5..cf510b87314 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -111,7 +111,7 @@ class RuckusAjaxApiPatchContext: def __init__( self, - login_mock: AsyncMock = None, + login_mock: AsyncMock | None = None, system_info: dict | None = None, mesh_info: dict | None = None, active_clients: list[dict] | AsyncMock | None = None, diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index ae0ccb0a9b1..5bfe2d941d5 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -11,7 +11,7 @@ from aioruckus.const import ( ) from aioruckus.exceptions import AuthenticationError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ruckus_unleashed.const import ( API_SYS_SYSINFO, API_SYS_SYSINFO_SERIAL, @@ -19,6 +19,7 @@ from homeassistant.components.ruckus_unleashed.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import utcnow from . import ( @@ -37,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -54,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_TITLE assert result2["data"] == CONFIG @@ -73,7 +74,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -96,7 +97,7 @@ async def test_form_user_reauth(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -111,7 +112,7 @@ async def test_form_user_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -134,7 +135,7 @@ async def test_form_user_reauth_different_unique_id(hass: HomeAssistant) -> None assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -151,7 +152,7 @@ async def test_form_user_reauth_different_unique_id(hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -174,7 +175,7 @@ async def test_form_user_reauth_invalid_auth(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -191,7 +192,7 @@ async def test_form_user_reauth_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -214,7 +215,7 @@ async def test_form_user_reauth_cannot_connect(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -231,7 +232,7 @@ async def test_form_user_reauth_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -254,7 +255,7 @@ async def test_form_user_reauth_general_exception(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -269,7 +270,7 @@ async def test_form_user_reauth_general_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -288,7 +289,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -304,7 +305,7 @@ async def test_form_general_exception(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -325,7 +326,7 @@ async def test_form_unexpected_response(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -348,7 +349,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -356,5 +357,5 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index e9e8446f8ac..c4ecf929f94 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -50,7 +50,7 @@ async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> data=init_data, context=init_context, ) - assert init_result["type"] == FlowResultType.FORM + assert init_result["type"] is FlowResultType.FORM assert init_result["step_id"] == config_entries.SOURCE_USER assert init_result["errors"] is None @@ -61,7 +61,7 @@ async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> entry, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == entry assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER @@ -80,7 +80,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: BASE_DATA, ) - assert config_result["type"] == FlowResultType.FORM + assert config_result["type"] is FlowResultType.FORM assert config_result["errors"] == {"base": "invalid_auth"} # Check that we still can finalize setup @@ -90,7 +90,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: BASE_DATA, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER @@ -109,7 +109,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: BASE_DATA, ) - assert config_result["type"] == FlowResultType.FORM + assert config_result["type"] is FlowResultType.FORM assert config_result["errors"] == {"base": "cannot_connect"} # Check that we still can finalize setup @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: BASE_DATA, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER @@ -138,7 +138,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: BASE_DATA, ) - assert config_result["type"] == FlowResultType.FORM + assert config_result["type"] is FlowResultType.FORM assert config_result["errors"] == {"base": "unknown"} # Check that we still can finalize setup @@ -148,7 +148,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: BASE_DATA, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py index 6f668b0168b..b6c79f1de0e 100644 --- a/tests/components/ruuvitag_ble/test_config_flow.py +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -26,7 +26,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True @@ -34,7 +34,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address @@ -46,7 +46,7 @@ async def test_async_step_bluetooth_not_ruuvitag(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -56,7 +56,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -70,7 +70,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True @@ -79,7 +79,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": RUUVITAG_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address @@ -94,7 +94,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -110,7 +110,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": RUUVITAG_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -132,7 +132,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -160,7 +160,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -168,7 +168,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -181,7 +181,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -192,7 +192,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True @@ -201,7 +201,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": RUUVITAG_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["data"] == {} assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address diff --git a/tests/components/rympro/test_config_flow.py b/tests/components/rympro/test_config_flow.py index f5591d8e0c7..e92b7c23357 100644 --- a/tests/components/rympro/test_config_flow.py +++ b/tests/components/rympro/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -67,7 +67,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA[CONF_EMAIL] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -99,7 +99,7 @@ async def test_login_error(hass: HomeAssistant, exception, error) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} with ( @@ -125,7 +125,7 @@ async def test_login_error(hass: HomeAssistant, exception, error) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_DATA[CONF_EMAIL] assert result3["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -156,7 +156,7 @@ async def test_form_already_exists(hass: HomeAssistant, config_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -171,7 +171,7 @@ async def test_form_reauth(hass: HomeAssistant, config_entry) -> None: }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -197,7 +197,7 @@ async def test_form_reauth(hass: HomeAssistant, config_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 @@ -214,7 +214,7 @@ async def test_form_reauth_with_new_account(hass: HomeAssistant, config_entry) - }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -240,7 +240,7 @@ async def test_form_reauth_with_new_account(hass: HomeAssistant, config_entry) - ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data[CONF_UNIQUE_ID] == "new-account-number" assert config_entry.unique_id == "new-account-number" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 2da1c7c87db..7f5394902b4 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from pysabnzbd import SabnzbdApiException import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sabnzbd import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( @@ -41,7 +41,7 @@ async def test_create_entry(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -54,7 +54,7 @@ async def test_create_entry(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "edc3eee7330e" assert result2["data"] == { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", @@ -91,7 +91,7 @@ async def test_import_flow(hass: HomeAssistant) -> None: data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "edc3eee7330e" assert result["data"][CONF_NAME] == "Sabnzbd" assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index a300c28b945..6c325ae3b04 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -43,6 +43,7 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_UDN, SsdpServiceInfo, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -214,7 +215,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # entry was added @@ -222,7 +223,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) # legacy tv entry created - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_name" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_name" @@ -243,7 +244,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # entry was added result2 = await hass.config_entries.flow.async_configure( @@ -257,7 +258,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: ) # legacy tv entry created - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "fake_name" assert result3["data"][CONF_HOST] == "fake_host" assert result3["data"][CONF_NAME] == "fake_name" @@ -277,7 +278,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # entry was added @@ -285,7 +286,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) # websocket tv entry created - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -304,7 +305,7 @@ async def test_user_encrypted_websocket( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -321,7 +322,7 @@ async def test_user_encrypted_websocket( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "encrypted_pairing" result3 = await hass.config_entries.flow.async_configure( @@ -334,7 +335,7 @@ async def test_user_encrypted_websocket( result3["flow_id"], user_input={"pin": "1234"} ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" assert result4["data"][CONF_NAME] == "TV-UE48JU6470" @@ -358,7 +359,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -370,7 +371,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -385,7 +386,7 @@ async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -406,7 +407,7 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -429,7 +430,7 @@ async def test_user_websocket_access_denied( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED assert "Please check the Device Connection Manager on your TV" in caplog.text @@ -451,7 +452,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} with ( @@ -466,7 +467,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -491,7 +492,7 @@ async def test_user_not_successful(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -511,7 +512,7 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -522,14 +523,14 @@ async def test_ssdp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -546,7 +547,7 @@ async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_NO_MANUFACTURER, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -561,7 +562,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -574,13 +575,13 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_NOPREFIX, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake2_model" assert result["data"][CONF_HOST] == "fake2_host" assert result["data"][CONF_NAME] == "fake2_model" @@ -600,14 +601,14 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # missing authentication result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -616,7 +617,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -636,7 +637,7 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -650,13 +651,13 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -680,13 +681,13 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -710,7 +711,7 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch( @@ -738,7 +739,7 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l result3["flow_id"], user_input={"pin": "1234"} ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" assert result4["data"][CONF_NAME] == "TV-UE48JU6470" @@ -768,7 +769,7 @@ async def test_ssdp_encrypted_websocket_not_supported( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -793,7 +794,7 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -807,7 +808,7 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_WRONGMODEL, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -832,14 +833,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # device not found result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -864,14 +865,14 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # device not found result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -886,14 +887,14 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # failed as already in progress result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_IN_PROGRESS @@ -908,7 +909,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] == "fake_model" @@ -918,7 +919,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == RESULT_ALREADY_CONFIGURED # check updated device info @@ -935,14 +936,14 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: data=MOCK_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "TV-UE48JU6470" @@ -964,14 +965,14 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: data=MOCK_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Samsung Frame (43) (UE43LS003)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Samsung Frame (43)" @@ -990,14 +991,14 @@ async def test_zeroconf(hass: HomeAssistant) -> None: data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_NAME] == "Living Room" @@ -1026,7 +1027,7 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -1039,7 +1040,7 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -1052,7 +1053,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: data=MOCK_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_init( @@ -1061,7 +1062,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -1103,7 +1104,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) @@ -1153,7 +1154,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc" @@ -1176,7 +1177,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -1191,7 +1192,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -1205,7 +1206,7 @@ async def test_autodetect_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] @@ -1217,7 +1218,7 @@ async def test_autodetect_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "legacy" assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_MAC] is None @@ -1239,7 +1240,7 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT assert remote.call_count == 1 assert remote.call_args_list == [ @@ -1268,7 +1269,7 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_CONFIGURED config_entries_domain = hass.config_entries.async_entries(DOMAIN) @@ -1297,7 +1298,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1321,7 +1322,7 @@ async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1345,7 +1346,7 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1371,7 +1372,7 @@ async def test_update_missing_model_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MODEL] == "fake_model" @@ -1392,7 +1393,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Wrong st @@ -1419,7 +1420,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "original" @@ -1448,7 +1449,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Wrong ST, ssdp location should not change @@ -1481,7 +1482,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST, ssdp location should change @@ -1516,7 +1517,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Main TV Agent ST, ssdp location should change @@ -1551,7 +1552,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST, ssdp location should be added @@ -1582,7 +1583,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST for MainTV, ssdp location should be added @@ -1613,7 +1614,7 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -1641,7 +1642,7 @@ async def test_update_legacy_missing_mac_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -1678,7 +1679,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id is None @@ -1704,7 +1705,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Wrong st @@ -1732,7 +1733,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct st @@ -1753,7 +1754,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -1761,7 +1762,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1770,14 +1771,14 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) entry.add_to_hass(hass) - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -1785,9 +1786,9 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED @pytest.mark.usefixtures("rest_api") @@ -1802,7 +1803,7 @@ async def test_form_reauth_websocket_cannot_connect( context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch.object(remotews, "open", side_effect=ConnectionFailure): @@ -1812,7 +1813,7 @@ async def test_form_reauth_websocket_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": RESULT_AUTH_MISSING} result3 = await hass.config_entries.flow.async_configure( @@ -1821,7 +1822,7 @@ async def test_form_reauth_websocket_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -1834,7 +1835,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -1847,7 +1848,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "not_supported" @@ -1860,14 +1861,14 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain=DOMAIN, data=encrypted_entry_data) entry.add_to_hass(hass) - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -1882,7 +1883,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -1891,14 +1892,14 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm_encrypted" # Invalid PIN result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pin": "invalid"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm_encrypted" # Valid PIN @@ -1906,9 +1907,9 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result["flow_id"], user_input={"pin": "1234"} ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED authenticator_mock.assert_called_once() assert authenticator_mock.call_args[0] == ("fake_host",) @@ -1945,7 +1946,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1971,7 +1972,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1998,7 +1999,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -2025,7 +2026,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -2042,7 +2043,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] == "fake_model" @@ -2059,7 +2060,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_CONFIGURED # ensure mac wasn't updated with "none" @@ -2076,7 +2077,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_CONFIGURED # ensure mac was updated with new wifiMac value diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 5bf8f2cacac..14c85b2c636 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -95,7 +95,7 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) assert len(config_entries_domain) == 1 - assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY + assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY @pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") @@ -166,7 +166,7 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: del encrypted_entry_data[CONF_SESSION_ID] entry = await setup_samsungtv_entry(hass, encrypted_entry_data) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR flows_in_progress = [ flow for flow in hass.config_entries.flow.async_progress() diff --git a/tests/components/sanix/__init__.py b/tests/components/sanix/__init__.py new file mode 100644 index 00000000000..ef1a9c63fbe --- /dev/null +++ b/tests/components/sanix/__init__.py @@ -0,0 +1,13 @@ +"""Tests for Sanix.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/sanix/conftest.py b/tests/components/sanix/conftest.py new file mode 100644 index 00000000000..d1f4424b166 --- /dev/null +++ b/tests/components/sanix/conftest.py @@ -0,0 +1,76 @@ +"""Sanix tests configuration.""" + +from collections.abc import Generator +from datetime import datetime +from unittest.mock import AsyncMock, patch +from zoneinfo import ZoneInfo + +import pytest +from sanix import ( + ATTR_API_BATTERY, + ATTR_API_DEVICE_NO, + ATTR_API_DISTANCE, + ATTR_API_FILL_PERC, + ATTR_API_SERVICE_DATE, + ATTR_API_SSID, + ATTR_API_STATUS, + ATTR_API_TIME, +) +from sanix.models import Measurement + +from homeassistant.components.sanix.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.const import CONF_TOKEN + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def mock_sanix(): + """Build a fixture for the Sanix API that connects successfully and returns measurements.""" + fixture = load_json_object_fixture("get_measurements.json", DOMAIN) + with ( + patch( + "homeassistant.components.sanix.config_flow.Sanix", + autospec=True, + ) as mock_sanix_api, + patch( + "homeassistant.components.sanix.Sanix", + new=mock_sanix_api, + ), + ): + mock_sanix_api.return_value.fetch_data.return_value = Measurement( + battery=fixture[ATTR_API_BATTERY], + device_no=fixture[ATTR_API_DEVICE_NO], + distance=fixture[ATTR_API_DISTANCE], + fill_perc=fixture[ATTR_API_FILL_PERC], + service_date=datetime.strptime( + fixture[ATTR_API_SERVICE_DATE], "%d.%m.%Y" + ).date(), + ssid=fixture[ATTR_API_SSID], + status=fixture[ATTR_API_STATUS], + time=datetime.strptime(fixture[ATTR_API_TIME], "%d.%m.%Y %H:%M:%S").replace( + tzinfo=ZoneInfo("Europe/Warsaw") + ), + ) + yield mock_sanix_api + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Sanix", + unique_id="1810088", + data={CONF_SERIAL_NUMBER: "1234", CONF_TOKEN: "abcd"}, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.sanix.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/sanix/fixtures/get_measurements.json b/tests/components/sanix/fixtures/get_measurements.json new file mode 100644 index 00000000000..de6f4c41311 --- /dev/null +++ b/tests/components/sanix/fixtures/get_measurements.json @@ -0,0 +1,10 @@ +{ + "device_no": "SANIX-1810088", + "status": "1", + "time": "30.12.2023 03:10:21", + "ssid": "Wifi", + "battery": "100", + "distance": "109", + "fill_perc": 32, + "service_date": "15.06.2024" +} diff --git a/tests/components/sanix/snapshots/test_sensor.ambr b/tests/components/sanix/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..84c97ce68b1 --- /dev/null +++ b/tests/components/sanix/snapshots/test_sensor.ambr @@ -0,0 +1,292 @@ +# serializer version: 1 +# name: test_all_entities[sensor.sanix_battery-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.sanix_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1810088-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sanix_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Sanix Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sanix_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.sanix_device_number-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.sanix_device_number', + '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': 'Device number', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_no', + 'unique_id': '1810088-device_no', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sanix_device_number-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sanix Device number', + }), + 'context': , + 'entity_id': 'sensor.sanix_device_number', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'SANIX-1810088', + }) +# --- +# name: test_all_entities[sensor.sanix_distance-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.sanix_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1810088-distance', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sanix_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Sanix Distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sanix_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '109', + }) +# --- +# name: test_all_entities[sensor.sanix_filled-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.sanix_filled', + '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': 'Filled', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fill_perc', + 'unique_id': '1810088-fill_perc', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sanix_filled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sanix Filled', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sanix_filled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '32', + }) +# --- +# name: test_all_entities[sensor.sanix_service_date-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.sanix_service_date', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service date', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'service_date', + 'unique_id': '1810088-service_date', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sanix_service_date-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'Sanix Service date', + }), + 'context': , + 'entity_id': 'sensor.sanix_service_date', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-06-15', + }) +# --- +# name: test_all_entities[sensor.sanix_ssid-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.sanix_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': 'SSID', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ssid', + 'unique_id': '1810088-ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sanix_ssid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sanix SSID', + }), + 'context': , + 'entity_id': 'sensor.sanix_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Wifi', + }) +# --- diff --git a/tests/components/sanix/test_config_flow.py b/tests/components/sanix/test_config_flow.py new file mode 100644 index 00000000000..abd91ee306c --- /dev/null +++ b/tests/components/sanix/test_config_flow.py @@ -0,0 +1,112 @@ +"""Define tests for the Sanix config flow.""" + +from unittest.mock import MagicMock + +import pytest +from sanix.exceptions import SanixException, SanixInvalidAuthException + +from homeassistant.components.sanix.const import ( + CONF_SERIAL_NUMBER, + DOMAIN, + MANUFACTURER, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +CONFIG = {CONF_SERIAL_NUMBER: "1810088", CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2"} + + +async def test_create_entry( + hass: HomeAssistant, mock_sanix: MagicMock, mock_setup_entry +) -> None: + """Test that the user step works.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == MANUFACTURER + assert result["data"] == { + CONF_SERIAL_NUMBER: "1810088", + CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (SanixInvalidAuthException("Invalid auth"), "invalid_auth"), + (SanixException("Something went wrong"), "unknown"), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, + exception: Exception, + error: str, + mock_sanix: MagicMock, + mock_setup_entry, +) -> None: + """Test Form exceptions.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_sanix.return_value.fetch_data.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + mock_sanix.return_value.fetch_data.side_effect = None + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Sanix" + assert result["data"] == { + CONF_SERIAL_NUMBER: "1810088", + CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_error( + hass: HomeAssistant, mock_sanix: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Test that errors are shown when duplicates are added.""" + + mock_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"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/sanix/test_init.py b/tests/components/sanix/test_init.py new file mode 100644 index 00000000000..467737628fe --- /dev/null +++ b/tests/components/sanix/test_init.py @@ -0,0 +1,27 @@ +"""Test the Sanix init module.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.sanix import setup_integration + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_sanix: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/sanix/test_sensor.py b/tests/components/sanix/test_sensor.py new file mode 100644 index 00000000000..d9729ca3c25 --- /dev/null +++ b/tests/components/sanix/test_sensor.py @@ -0,0 +1,39 @@ +"""Test the Sanix sensor module.""" + +from unittest.mock import AsyncMock, patch + +import pytest +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 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_sanix: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.sanix.PLATFORMS", + [Platform.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 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/schlage/test_config_flow.py b/tests/components/schlage/test_config_flow.py index 118ae44d15b..15ef3858c0c 100644 --- a/tests/components/schlage/test_config_flow.py +++ b/tests/components/schlage/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -35,7 +35,7 @@ async def test_form( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -60,7 +60,7 @@ async def test_form_invalid_auth( "password": "test-password", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -79,7 +79,7 @@ async def test_form_unknown(hass: HomeAssistant, mock_pyschlage_auth: Mock) -> N }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -105,7 +105,7 @@ async def test_reauth( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_added_config_entry.data == { "username": "asdf@asdf.com", @@ -138,7 +138,7 @@ async def test_reauth_invalid_auth( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -165,7 +165,7 @@ async def test_reauth_wrong_account( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "wrong_account" assert mock_added_config_entry.data == { "username": "asdf@asdf.com", diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index 8e281e148fc..17a527d2975 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -49,7 +49,7 @@ async def test_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.rest.RestData", @@ -75,7 +75,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["version"] == 1 assert result3["options"] == { CONF_RESOURCE: "https://www.home-assistant.io", @@ -106,7 +106,7 @@ async def test_form_with_post( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.rest.RestData", @@ -133,7 +133,7 @@ async def test_form_with_post( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["version"] == 1 assert result3["options"] == { CONF_RESOURCE: "https://www.home-assistant.io", @@ -165,7 +165,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -224,7 +224,7 @@ async def test_flow_fails( ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "https://www.home-assistant.io" assert result4["options"] == { CONF_RESOURCE: "https://www.home-assistant.io", @@ -253,7 +253,7 @@ async def test_options_resource_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -261,7 +261,7 @@ async def test_options_resource_flow( {"next_step_id": "resource"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "resource" mocker = MockRestData("test_scrape_sensor2") @@ -280,7 +280,7 @@ async def test_options_resource_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -319,7 +319,7 @@ async def test_options_add_remove_sensor_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -327,7 +327,7 @@ async def test_options_add_remove_sensor_flow( {"next_step_id": "add_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" mocker = MockRestData("test_scrape_sensor2") @@ -348,7 +348,7 @@ async def test_options_add_remove_sensor_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -387,7 +387,7 @@ async def test_options_add_remove_sensor_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -395,7 +395,7 @@ async def test_options_add_remove_sensor_flow( {"next_step_id": "remove_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_sensor" mocker = MockRestData("test_scrape_sensor2") @@ -408,7 +408,7 @@ async def test_options_add_remove_sensor_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -445,7 +445,7 @@ async def test_options_edit_sensor_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -453,7 +453,7 @@ async def test_options_edit_sensor_flow( {"next_step_id": "select_edit_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_sensor" result = await hass.config_entries.options.async_configure( @@ -461,7 +461,7 @@ async def test_options_edit_sensor_flow( {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_sensor" mocker = MockRestData("test_scrape_sensor2") @@ -475,7 +475,7 @@ async def test_options_edit_sensor_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -529,21 +529,21 @@ async def test_sensor_options_add_device_class( entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_sensor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_sensor" result = await hass.config_entries.options.async_configure( @@ -559,7 +559,7 @@ async def test_sensor_options_add_device_class( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -611,21 +611,21 @@ async def test_sensor_options_remove_device_class( entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_sensor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_sensor" result = await hass.config_entries.options.async_configure( @@ -638,7 +638,7 @@ async def test_sensor_options_remove_device_class( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py index 8ad766a80bd..db1a89e1ce4 100644 --- a/tests/components/scrape/test_init.py +++ b/tests/components/scrape/test_init.py @@ -6,8 +6,8 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -117,16 +117,16 @@ async def test_setup_config_no_sensors( async def test_setup_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test setup entry.""" - assert loaded_entry.state == config_entries.ConfigEntryState.LOADED + assert loaded_entry.state is ConfigEntryState.LOADED async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test unload an entry.""" - assert loaded_entry.state == config_entries.ConfigEntryState.LOADED + assert loaded_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(loaded_entry.entry_id) await hass.async_block_till_done() - assert loaded_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert loaded_entry.state is ConfigEntryState.NOT_LOADED async def remove_device(ws_client, device_id, config_entry_id): diff --git a/tests/components/screenlogic/__init__.py b/tests/components/screenlogic/__init__.py index c9889e6b4b8..e562b84ad14 100644 --- a/tests/components/screenlogic/__init__.py +++ b/tests/components/screenlogic/__init__.py @@ -61,7 +61,7 @@ async def stub_async_connect( gtype=None, gsubtype=None, name=MOCK_ADAPTER_NAME, - connection_closed_callback: Callable = None, + connection_closed_callback: Callable | None = None, ) -> bool: """Initialize minimum attributes needed for tests.""" self._ip = ip diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index be1617e3105..8ca6bd4cb90 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -24,6 +24,7 @@ from homeassistant.components.screenlogic.const import ( ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +48,7 @@ async def test_flow_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_select" @@ -60,7 +61,7 @@ async def test_flow_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pentair: 01-01-01" assert result2["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -80,7 +81,7 @@ async def test_flow_discover_none(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_entry" @@ -96,7 +97,7 @@ async def test_flow_discover_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_entry" @@ -119,7 +120,7 @@ async def test_flow_discover_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Pentair: 01-01-01" assert result3["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -141,7 +142,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "gateway_entry" with ( @@ -163,7 +164,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Pentair: 01-01-01" assert result3["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -190,7 +191,7 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_select" @@ -198,7 +199,7 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: result["flow_id"], user_input={GATEWAY_SELECT_KEY: GATEWAY_MANUAL_ENTRY} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} assert result2["step_id"] == "gateway_entry" @@ -221,7 +222,7 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Pentair: 01-01-01" assert result3["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -252,7 +253,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -270,14 +271,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 15}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_SCAN_INTERVAL: 15} @@ -295,13 +296,13 @@ async def test_option_flow_defaults(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, } @@ -321,13 +322,13 @@ async def test_option_flow_input_floor(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL, } diff --git a/tests/components/screenlogic/test_init.py b/tests/components/screenlogic/test_init.py index 9c296fd8afd..6aab9ecec93 100644 --- a/tests/components/screenlogic/test_init.py +++ b/tests/components/screenlogic/test_init.py @@ -89,9 +89,9 @@ TEST_MIGRATING_ENTITIES = [ ), ] -MIGRATION_CONNECT = lambda *args, **kwargs: stub_async_connect( - DATA_MIN_MIGRATION, *args, **kwargs -) + +def _migration_connect(*args, **kwargs): + return stub_async_connect(DATA_MIN_MIGRATION, *args, **kwargs) @pytest.mark.parametrize( @@ -164,7 +164,7 @@ async def test_async_migrate_entries( ), patch.multiple( ScreenLogicGateway, - async_connect=MIGRATION_CONNECT, + async_connect=_migration_connect, is_connected=True, _async_connected_request=DEFAULT, ), @@ -236,7 +236,7 @@ async def test_entity_migration_data( ), patch.multiple( ScreenLogicGateway, - async_connect=MIGRATION_CONNECT, + async_connect=_migration_connect, is_connected=True, _async_connected_request=DEFAULT, ), @@ -257,9 +257,9 @@ async def test_platform_setup( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test setup for platforms that define expected data.""" - stub_connect = lambda *args, **kwargs: stub_async_connect( - DATA_MISSING_VALUES_CHEM_CHLOR, *args, **kwargs - ) + + def stub_connect(*args, **kwargs): + return stub_async_connect(DATA_MISSING_VALUES_CHEM_CHLOR, *args, **kwargs) device_prefix = slugify(MOCK_ADAPTER_NAME) diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index bccf1d9aa50..b956aa588cb 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -8,9 +8,9 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components import script from homeassistant.components.blueprint.models import Blueprint, DomainBlueprints +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, template from homeassistant.setup import async_setup_component @@ -48,7 +48,7 @@ async def test_confirmable_notification( ) -> None: """Test confirmable notification blueprint.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) frodo = device_registry.async_get_or_create( diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index ba448230c35..790ef7e79bc 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -7,9 +7,9 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries from homeassistant.components import script from homeassistant.components.script import DOMAIN, EVENT_SCRIPT_STARTED, ScriptEntity +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -729,7 +729,7 @@ async def test_extraction_functions( ) -> None: """Test extraction functions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_in_both = device_registry.async_get_or_create( diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index e0a140f7136..2bc6a780c1b 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -20,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_full_user_flow( user_input={CONF_TYPE: TYPE_ASTRONOMICAL}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Season" assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL} @@ -44,5 +44,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": SOURCE_USER}, data={CONF_TYPE: TYPE_ASTRONOMICAL} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/select/common.py b/tests/components/select/common.py new file mode 100644 index 00000000000..c2a401a038b --- /dev/null +++ b/tests/components/select/common.py @@ -0,0 +1,23 @@ +"""Common helpers for select entity component tests.""" + +from homeassistant.components.select import SelectEntity + +from tests.common import MockEntity + + +class MockSelectEntity(MockEntity, SelectEntity): + """Mock Select class.""" + + @property + def current_option(self): + """Return the current option of this select.""" + return self._handle("current_option") + + @property + def options(self) -> list: + """Return the list of available options of this select.""" + return self._handle("options") + + def select_option(self, option: str) -> None: + """Change the selected option.""" + self._values["current_option"] = option diff --git a/tests/components/select/conftest.py b/tests/components/select/conftest.py new file mode 100644 index 00000000000..700749f9aba --- /dev/null +++ b/tests/components/select/conftest.py @@ -0,0 +1,24 @@ +"""Fixtures for the select entity component tests.""" + +import pytest + +from tests.components.select.common import MockSelectEntity + + +@pytest.fixture +def mock_select_entities() -> list[MockSelectEntity]: + """Return a list of mock select entities.""" + return [ + MockSelectEntity( + name="select 1", + unique_id="unique_select_1", + options=["option 1", "option 2", "option 3"], + current_option="option 1", + ), + MockSelectEntity( + name="select 2", + unique_id="unique_select_2", + options=["option 1", "option 2", "option 3"], + current_option=None, + ), + ] diff --git a/tests/components/select/test_init.py b/tests/components/select/test_init.py index b135a6e1ab0..a5be7921fcd 100644 --- a/tests/components/select/test_init.py +++ b/tests/components/select/test_init.py @@ -21,6 +21,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component +from tests.common import setup_test_component_platform + class MockSelectEntity(SelectEntity): """Mock SelectEntity to use in tests.""" @@ -91,11 +93,11 @@ async def test_select(hass: HomeAssistant) -> None: async def test_custom_integration_and_validation( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_select_entities: list[MockSelectEntity], ) -> None: """Test we can only select valid options.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_select_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index dc1cee43662..e564603ea87 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components.sense.const import DOMAIN from homeassistant.const import CONF_CODE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant, mock_sense) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -64,7 +65,7 @@ async def test_form(hass: HomeAssistant, mock_sense) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-email" assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +86,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -102,7 +103,7 @@ async def test_form_mfa_required(hass: HomeAssistant, mock_sense) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = None @@ -111,7 +112,7 @@ async def test_form_mfa_required(hass: HomeAssistant, mock_sense) -> None: {CONF_CODE: "012345"}, ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-email" assert result3["data"] == MOCK_CONFIG @@ -129,7 +130,7 @@ async def test_form_mfa_required_wrong(hass: HomeAssistant, mock_sense) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = SenseAuthenticationException @@ -139,7 +140,7 @@ async def test_form_mfa_required_wrong(hass: HomeAssistant, mock_sense) -> None: {CONF_CODE: "000000"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "invalid_auth"} assert result3["step_id"] == "validation" @@ -157,7 +158,7 @@ async def test_form_mfa_required_timeout(hass: HomeAssistant, mock_sense) -> Non {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = SenseAPITimeoutException @@ -166,7 +167,7 @@ async def test_form_mfa_required_timeout(hass: HomeAssistant, mock_sense) -> Non {CONF_CODE: "000000"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -183,7 +184,7 @@ async def test_form_mfa_required_exception(hass: HomeAssistant, mock_sense) -> N {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = Exception @@ -192,7 +193,7 @@ async def test_form_mfa_required_exception(hass: HomeAssistant, mock_sense) -> N {CONF_CODE: "000000"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "unknown"} @@ -211,7 +212,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -230,7 +231,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -249,7 +250,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -270,7 +271,7 @@ async def test_reauth_no_form(hass: HomeAssistant, mock_sense) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=MOCK_CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -290,7 +291,7 @@ async def test_reauth_password(hass: HomeAssistant, mock_sense) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM mock_sense.return_value.authenticate.side_effect = None with patch( @@ -303,5 +304,5 @@ async def test_reauth_password(hass: HomeAssistant, mock_sense) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 3b1117f0908..e994402b09f 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -51,7 +51,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["version"] == 2 assert result2["data"] == { "api_key": "1234567890", @@ -78,7 +78,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -115,7 +115,7 @@ async def test_flow_fails( }, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Sensibo" assert result3["data"] == { "api_key": "1234567891", @@ -129,7 +129,7 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -159,7 +159,7 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -202,7 +202,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -225,7 +225,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -275,7 +275,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -298,7 +298,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -348,7 +348,7 @@ async def test_flow_reauth_no_username_or_device( data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -370,5 +370,5 @@ async def test_flow_reauth_no_username_or_device( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py index 9698d5241cc..9ab30edf177 100644 --- a/tests/components/sensibo/test_init.py +++ b/tests/components/sensibo/test_init.py @@ -6,10 +6,9 @@ from unittest.mock import patch from pysensibo.model import SensiboData -from homeassistant import config_entries from homeassistant.components.sensibo.const import DOMAIN from homeassistant.components.sensibo.util import NoUsernameError -from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.config_entries import SOURCE_USER, ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -49,7 +48,7 @@ async def test_setup_entry(hass: HomeAssistant, get_data: SensiboData) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None: @@ -81,7 +80,7 @@ async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.version == 2 assert entry.unique_id == "username" @@ -113,7 +112,7 @@ async def test_migrate_entry_fails(hass: HomeAssistant, get_data: SensiboData) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == 1 assert entry.unique_id == "12" @@ -147,10 +146,10 @@ async def test_unload_entry(hass: HomeAssistant, get_data: SensiboData) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def remove_device(ws_client, device_id, config_entry_id): diff --git a/tests/components/sensirion_ble/test_config_flow.py b/tests/components/sensirion_ble/test_config_flow.py index e93d060fd3e..00e92d37118 100644 --- a/tests/components/sensirion_ble/test_config_flow.py +++ b/tests/components/sensirion_ble/test_config_flow.py @@ -30,7 +30,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address @@ -50,7 +50,7 @@ async def test_async_step_bluetooth_not_sensirion(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -60,7 +60,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -74,7 +74,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True @@ -83,7 +83,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": SENSIRION_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address @@ -98,7 +98,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -114,7 +114,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": SENSIRION_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -136,7 +136,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -153,7 +153,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -164,7 +164,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -172,7 +172,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -185,7 +185,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True @@ -205,7 +205,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": SENSIRION_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["data"] == {} assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index b633c744205..2a142633ab3 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -545,8 +545,10 @@ async def test_if_state_above( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } @@ -612,8 +614,10 @@ async def test_if_state_above_legacy( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } @@ -679,8 +683,10 @@ async def test_if_state_below( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } @@ -747,8 +753,10 @@ async def test_if_state_between( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 98bea960fcc..49e00a927b4 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -496,15 +496,12 @@ async def test_if_fires_on_state_above( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -564,15 +561,12 @@ async def test_if_fires_on_state_below( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -633,15 +627,12 @@ async def test_if_fires_on_state_between( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -712,15 +703,12 @@ async def test_if_fires_on_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -781,15 +769,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0ecb4b9c60f..079984476b0 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1146,6 +1146,14 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, suggested_unit_of_measurement=suggested_unit, ) + entity4 = MockSensor( + name="Test", + device_class=device_class, + native_unit_of_measurement=native_unit, + native_value=str(native_value), + suggested_display_precision=None, + unique_id="very_unique_4", + ) setup_test_component_platform( hass, sensor.DOMAIN, @@ -1154,6 +1162,7 @@ async def test_unit_conversion_priority_precision( entity1, entity2, entity3, + entity4, ], ) @@ -1230,6 +1239,21 @@ async def test_unit_conversion_priority_precision( round(custom_state, 4) ) + # Set a display_precision without having suggested_display_precision + entity_registry.async_update_entity_options( + entity4.entity_id, + "sensor", + {"display_precision": 4}, + ) + entry4 = entity_registry.async_get(entity4.entity_id) + assert "suggested_display_precision" not in entry4.options["sensor"] + assert entry4.options["sensor"]["display_precision"] == 4 + await hass.async_block_till_done() + state = hass.states.get(entity4.entity_id) + assert float(async_rounded_state(hass, entity4.entity_id, state)) == pytest.approx( + round(automatic_state, 4) + ) + @pytest.mark.parametrize( ( @@ -1594,6 +1618,41 @@ async def test_suggested_precision_option_update( } +async def test_suggested_precision_option_removal( + hass: HomeAssistant, +) -> None: + """Test suggested precision stored in the registry is removed.""" + + entity_registry = er.async_get(hass) + + # Pre-register entities + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, + "sensor", + { + "suggested_display_precision": 1, + }, + ) + + entity0 = MockSensor( + name="Test", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.HOURS, + native_value="1.5", + suggested_display_precision=None, + unique_id="very_unique", + ) + 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 the suggested precision is no longer stored in the registry + entry = entity_registry.async_get(entity0.entity_id) + assert entry.options.get("sensor", {}).get("suggested_display_precision") is None + + @pytest.mark.parametrize( ( "unit_system", @@ -2630,7 +2689,7 @@ async def test_suggested_unit_guard_valid_unit( native_unit: str, native_value: int, suggested_unit: str, - expect_value: float | int, + expect_value: float, ) -> None: """Test suggested_unit_of_measurement guard. diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8084fe69e89..a7aaf938410 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -560,7 +560,7 @@ def test_compile_hourly_statistics_purged_state_changes( ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - mean = min = max = float(hist["sensor.test1"][-1].state) + mean = min_value = max_value = float(hist["sensor.test1"][-1].state) # Purge all states from the database with freeze_time(four): @@ -594,8 +594,8 @@ def test_compile_hourly_statistics_purged_state_changes( "start": process_timestamp(zero).timestamp(), "end": process_timestamp(zero + timedelta(minutes=5)).timestamp(), "mean": pytest.approx(mean), - "min": pytest.approx(min), - "max": pytest.approx(max), + "min": pytest.approx(min_value), + "max": pytest.approx(max_value), "last_reset": None, "state": None, "sum": None, @@ -4113,12 +4113,12 @@ async def test_validate_unit_change_convertible( The test also asserts that the sensor's device class is ignored. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4228,12 +4228,12 @@ async def test_validate_statistics_unit_ignore_device_class( The test asserts that the sensor's device class is ignored. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4321,14 +4321,14 @@ async def test_validate_statistics_unit_change_no_device_class( conversion, and the unit is then changed to a unit which can and cannot be converted to the original unit. """ - id = 1 + msg_id = 1 attributes = dict(attributes) attributes.pop("device_class") def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4436,12 +4436,12 @@ async def test_validate_statistics_unsupported_state_class( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4505,12 +4505,12 @@ async def test_validate_statistics_sensor_no_longer_recorded( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4573,12 +4573,12 @@ async def test_validate_statistics_sensor_not_recorded( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4638,12 +4638,12 @@ async def test_validate_statistics_sensor_removed( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4702,12 +4702,12 @@ async def test_validate_statistics_unit_change_no_conversion( unit2, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4837,12 +4837,12 @@ async def test_validate_statistics_unit_change_equivalent_units( This tests no validation issue is created when a sensor's unit changes to an equivalent unit. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4923,12 +4923,12 @@ async def test_validate_statistics_unit_change_equivalent_units_2( equivalent unit which is not known to the unit converters. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -5005,12 +5005,12 @@ async def test_validate_statistics_other_domain( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test sensor does not raise issues for statistics for other domains.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( diff --git a/tests/components/sensorpro/test_config_flow.py b/tests/components/sensorpro/test_config_flow.py index 1558e774f21..05be86d5209 100644 --- a/tests/components/sensorpro/test_config_flow.py +++ b/tests/components/sensorpro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.sensorpro.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "T201 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_sensorpro(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.sensorpro.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "T201 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.sensorpro.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "T201 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index abbe04178c2..7e87dd1c6b8 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTPWX_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.sensorpush.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "HTP.xw F4D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_sensorpush(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSOR_PUSH_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.sensorpush.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "HT.w 0CA1" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.sensorpush.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "HT.w 0CA1" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 0c3fc45b68b..bd24023ac5e 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -29,7 +29,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} with ( @@ -44,7 +44,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: {"dsn": "http://public@sentry.local/1"}, ) - assert result2.get("type") == "create_entry" + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Sentry" assert result2.get("data") == { "dsn": "http://public@sentry.local/1", @@ -61,7 +61,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -80,7 +80,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "bad_dsn"} @@ -99,7 +99,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -117,7 +117,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result = await hass.config_entries.options.async_configure( @@ -134,7 +134,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, diff --git a/tests/components/seventeentrack/__init__.py b/tests/components/seventeentrack/__init__.py index 4101f34496e..b3452b38f96 100644 --- a/tests/components/seventeentrack/__init__.py +++ b/tests/components/seventeentrack/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory -from homeassistant.components.seventeentrack.sensor import DEFAULT_SCAN_INTERVAL +from homeassistant.components.seventeentrack.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/seventeentrack/conftest.py b/tests/components/seventeentrack/conftest.py index 052d66a4696..2e266a9b13c 100644 --- a/tests/components/seventeentrack/conftest.py +++ b/tests/components/seventeentrack/conftest.py @@ -1,19 +1,16 @@ """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, + DEFAULT_SHOW_ARCHIVED, + DEFAULT_SHOW_DELIVERED, ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -29,6 +26,8 @@ DEFAULT_SUMMARY = { "Returned": 0, } +DEFAULT_SUMMARY_LENGTH = len(DEFAULT_SUMMARY) + ACCOUNT_ID = "1234" NEW_SUMMARY_DATA = { @@ -129,7 +128,7 @@ def mock_seventeentrack(): def get_package( tracking_number: str = "456", destination_country: int = 206, - friendly_name: Optional[str] = "friendly name 1", + friendly_name: str | None = "friendly name 1", info_text: str = "info text 1", location: str = "location 1", timestamp: str = "2020-08-10 10:32", diff --git a/tests/components/seventeentrack/snapshots/test_services.ambr b/tests/components/seventeentrack/snapshots/test_services.ambr new file mode 100644 index 00000000000..185a1d44fe0 --- /dev/null +++ b/tests/components/seventeentrack/snapshots/test_services.ambr @@ -0,0 +1,53 @@ +# serializer version: 1 +# name: test_get_all_packages + dict({ + 'packages': list([ + dict({ + 'friendly_name': 'friendly name 3', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'Expired', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '123', + }), + dict({ + 'friendly_name': 'friendly name 1', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'In Transit', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '456', + }), + dict({ + 'friendly_name': 'friendly name 2', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'Delivered', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '789', + }), + ]), + }) +# --- +# name: test_get_packages_from_list + dict({ + 'packages': list([ + dict({ + 'friendly_name': 'friendly name 1', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'In Transit', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '456', + }), + dict({ + 'friendly_name': 'friendly name 2', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'Delivered', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '789', + }), + ]), + }) +# --- diff --git a/tests/components/seventeentrack/test_config_flow.py b/tests/components/seventeentrack/test_config_flow.py index ae48fb6c792..380146ed276 100644 --- a/tests/components/seventeentrack/test_config_flow.py +++ b/tests/components/seventeentrack/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from py17track.errors import SeventeenTrackError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.seventeentrack import DOMAIN from homeassistant.components.seventeentrack.const import ( CONF_SHOW_ARCHIVED, @@ -38,7 +38,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -47,7 +47,7 @@ async def test_create_entry( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "someemail@gmail.com" assert result2["data"] == { CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", @@ -97,7 +97,7 @@ async def test_flow_fails( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "someemail@gmail.com" assert result["data"] == { CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", @@ -113,7 +113,7 @@ async def test_import_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "someemail@gmail.com" assert result["data"][CONF_USERNAME] == "someemail@gmail.com" assert result["data"][CONF_PASSWORD] == "edc3eee7330e4fdda04489e3fbc283d0" @@ -150,7 +150,7 @@ async def test_import_flow_cannot_connect_error( data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -170,7 +170,7 @@ async def test_option_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -178,7 +178,7 @@ async def test_option_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) user_input={CONF_SHOW_ARCHIVED: True, CONF_SHOW_DELIVERED: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_SHOW_ARCHIVED] assert not result["data"][CONF_SHOW_DELIVERED] @@ -204,5 +204,5 @@ async def test_import_flow_already_configured( ) await hass.async_block_till_done() - assert result_aborted["type"] == data_entry_flow.FlowResultType.ABORT + assert result_aborted["type"] is 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 aa7f61ad318..31fc5deec24 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.setup import async_setup_component from . import goto_future, init_integration from .conftest import ( DEFAULT_SUMMARY, + DEFAULT_SUMMARY_LENGTH, NEW_SUMMARY_DATA, VALID_PLATFORM_CONFIG_FULL, get_package, @@ -72,11 +73,10 @@ async def test_add_package( """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 + assert hass.states.get("sensor.17track_package_friendly_name_1") + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package2 = get_package( tracking_number="789", @@ -89,8 +89,8 @@ async def test_add_package( await goto_future(hass, freezer) - assert hass.states.get("sensor.seventeentrack_package_789") is not None - assert len(hass.states.async_entity_ids()) == 2 + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 async def test_add_package_default_friendly_name( @@ -101,13 +101,12 @@ async def test_add_package_default_friendly_name( """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") + state_456 = hass.states.get("sensor.17track_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 + assert state_456.attributes["friendly_name"] == "17Track Package 456" + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 async def test_remove_package( @@ -130,26 +129,20 @@ async def test_remove_package( package1, package2, ] - 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 hass.states.get("sensor.seventeentrack_package_789") is not None - assert len(hass.states.async_entity_ids()) == 2 + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None + assert hass.states.get("sensor.17track_package_friendly_name_2") is not None + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 mock_seventeentrack.return_value.profile.packages.return_value = [package2] 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 + assert hass.states.get("sensor.17track_package_friendly_name_1") is None + assert hass.states.get("sensor.17track_package_friendly_name_2") is not None + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 async def test_package_error( @@ -164,36 +157,7 @@ async def test_package_error( mock_seventeentrack.return_value.profile.summary.return_value = {} 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 = get_package(friendly_name="friendly name 2") - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await goto_future(hass, freezer) - - assert hass.states.get("sensor.seventeentrack_package_456") is not None - entity = hass.data["entity_components"]["sensor"].get_entity( - "sensor.seventeentrack_package_456" - ) - assert entity.name == "Seventeentrack Package: friendly name 2" - assert len(hass.states.async_entity_ids()) == 1 + assert hass.states.get("sensor.17track_package_friendly_name_1") is None async def test_delivered_not_shown( @@ -205,7 +169,6 @@ async def test_delivered_not_shown( """Ensure delivered packages are not shown.""" 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" @@ -213,7 +176,7 @@ async def test_delivered_not_shown( await init_integration(hass, mock_config_entry_with_default_options) await goto_future(hass, freezer) - assert not hass.states.async_entity_ids() + assert hass.states.get("sensor.17track_package_friendly_name_1") is None persistent_notification_mock.create.assert_called() @@ -225,15 +188,14 @@ async def test_delivered_shown( """Ensure delivered packages are show when user choose to show them.""" 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 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 hass.states.get("sensor.17track_package_friendly_name_1") is not None + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 persistent_notification_mock.create.assert_not_called() @@ -246,12 +208,11 @@ async def test_becomes_delivered_not_shown_notification( """Ensure notification is triggered when package becomes delivered.""" 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_with_default_options) - assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package_delivered = get_package(status=40) mock_seventeentrack.return_value.profile.packages.return_value = [package_delivered] @@ -260,10 +221,9 @@ async def test_becomes_delivered_not_shown_notification( "homeassistant.components.seventeentrack.sensor.persistent_notification" ) as persistent_notification_mock: await goto_future(hass, freezer) - await goto_future(hass, freezer) persistent_notification_mock.create.assert_called() - assert not hass.states.async_entity_ids() + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH async def test_summary_correctly_updated( @@ -275,33 +235,27 @@ async def test_summary_correctly_updated( """Ensure summary entities are not duplicated.""" 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 init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 8 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - state_ready_picked = hass.states.get( - "sensor.seventeentrack_packages_ready_to_be_picked_up" - ) + state_ready_picked = hass.states.get("sensor.17track_ready_to_be_picked_up") assert state_ready_picked is not None assert len(state_ready_picked.attributes["packages"]) == 1 mock_seventeentrack.return_value.profile.packages.return_value = [] mock_seventeentrack.return_value.profile.summary.return_value = NEW_SUMMARY_DATA - await goto_future(hass, freezer) await goto_future(hass, freezer) - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == len(NEW_SUMMARY_DATA) 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" - ) + state_ready_picked = hass.states.get("sensor.17track_ready_to_be_picked_up") assert state_ready_picked is not None - assert state_ready_picked.attributes["packages"] is None + assert len(state_ready_picked.attributes["packages"]) == 0 async def test_summary_error( @@ -318,7 +272,7 @@ async def test_summary_error( await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == 0 assert ( hass.states.get("sensor.seventeentrack_packages_ready_to_be_picked_up") is None @@ -334,13 +288,12 @@ async def test_utc_timestamp( 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 - state_456 = hass.states.get("sensor.seventeentrack_package_456") + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 + state_456 = hass.states.get("sensor.17track_package_friendly_name_1") assert state_456 is not None assert str(state_456.attributes.get("timestamp")) == "2020-08-10 03:32:00+00:00" diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py new file mode 100644 index 00000000000..cbd7132bf67 --- /dev/null +++ b/tests/components/seventeentrack/test_services.py @@ -0,0 +1,76 @@ +"""Tests for the seventeentrack service.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.components.seventeentrack import DOMAIN, SERVICE_GET_PACKAGES +from homeassistant.core import HomeAssistant, SupportsResponse + +from tests.common import MockConfigEntry +from tests.components.seventeentrack import init_integration +from tests.components.seventeentrack.conftest import get_package + + +async def test_get_packages_from_list( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure service returns only the packages in the list.""" + await _mock_packages(mock_seventeentrack) + await init_integration(hass, mock_config_entry) + service_response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": mock_config_entry.entry_id, + "package_state": ["in_transit", "delivered"], + }, + blocking=True, + return_response=SupportsResponse.ONLY, + ) + + assert service_response == snapshot + + +async def test_get_all_packages( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure service returns all packages when non provided.""" + await _mock_packages(mock_seventeentrack) + await init_integration(hass, mock_config_entry) + service_response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": mock_config_entry.entry_id, + }, + blocking=True, + return_response=SupportsResponse.ONLY, + ) + + assert service_response == snapshot + + +async def _mock_packages(mock_seventeentrack): + package1 = get_package(status=10) + package2 = get_package( + tracking_number="789", + friendly_name="friendly name 2", + status=40, + ) + package3 = get_package( + tracking_number="123", + friendly_name="friendly name 3", + status=20, + ) + mock_seventeentrack.return_value.profile.packages.return_value = [ + package1, + package2, + package3, + ] diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index 282d7dbbb4c..08c12e9817b 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -7,11 +7,12 @@ import pytest from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError from sfrbox_api.models import SystemInfo -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import load_fixture @@ -25,7 +26,7 @@ async def test_config_flow_skip_auth( 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["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -39,7 +40,7 @@ async def test_config_flow_skip_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -55,7 +56,7 @@ async def test_config_flow_skip_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_auth" result = await hass.config_entries.flow.async_configure( @@ -63,7 +64,7 @@ async def test_config_flow_skip_auth( {"next_step_id": "skip_auth"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SFR Box" assert result["data"] == {CONF_HOST: "192.168.0.1"} @@ -77,7 +78,7 @@ async def test_config_flow_with_auth( 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["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -93,7 +94,7 @@ async def test_config_flow_with_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_auth" result = await hass.config_entries.flow.async_configure( @@ -113,7 +114,7 @@ async def test_config_flow_with_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): @@ -125,7 +126,7 @@ async def test_config_flow_with_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SFR Box" assert result["data"] == { CONF_HOST: "192.168.0.1", @@ -146,7 +147,7 @@ async def test_config_flow_duplicate_host( 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["type"] is FlowResultType.FORM assert result["errors"] == {} system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) @@ -163,7 +164,7 @@ async def test_config_flow_duplicate_host( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -180,7 +181,7 @@ async def test_config_flow_duplicate_mac( 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["type"] is FlowResultType.FORM assert result["errors"] == {} system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) @@ -195,7 +196,7 @@ async def test_config_flow_duplicate_mac( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -216,7 +217,7 @@ async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) data=config_entry_with_auth.data, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} # Failed credentials @@ -232,7 +233,7 @@ async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) }, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "invalid_auth"} # Valid credentials @@ -245,5 +246,5 @@ async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) }, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" diff --git a/tests/components/sharkiq/const.py b/tests/components/sharkiq/const.py index b4f9d72dafd..e8d920e7763 100644 --- a/tests/components/sharkiq/const.py +++ b/tests/components/sharkiq/const.py @@ -65,6 +65,11 @@ SHARK_PROPERTIES_DICT = { "read_only": True, "value": "Dummy Firmware 1.0", }, + "Robot_Room_List": { + "base_type": "string", + "read_only": True, + "value": "Kitchen", + }, } TEST_USERNAME = "test-username" diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index a81c185fd71..cf75bff1686 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -9,6 +9,7 @@ from sharkiq import AylaApi, SharkIqAuthError, SharkIqError from homeassistant import config_entries from homeassistant.components.sharkiq.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from .const import ( @@ -41,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -56,7 +57,7 @@ async def test_form(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{TEST_USERNAME:s}" assert result2["data"] == { "username": TEST_USERNAME, @@ -89,7 +90,7 @@ async def test_form_error(hass: HomeAssistant, exc: Exception, base_error: str) CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"].get("base") == base_error @@ -105,7 +106,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 4a1671a616f..c72ad1a8c36 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -11,7 +11,9 @@ from unittest.mock import patch import pytest from sharkiq import AylaApi, SharkIqAuthError, SharkIqNotAuthedError, SharkIqVacuum +from voluptuous.error import MultipleInvalid +from homeassistant import exceptions from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY from homeassistant.components.sharkiq import DOMAIN from homeassistant.components.sharkiq.vacuum import ( @@ -19,7 +21,9 @@ from homeassistant.components.sharkiq.vacuum import ( ATTR_ERROR_MSG, ATTR_LOW_LIGHT, ATTR_RECHARGE_RESUME, + ATTR_ROOMS, FAN_SPEEDS_MAP, + SERVICE_CLEAN_ROOM, ) from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, @@ -58,6 +62,7 @@ from .const import ( from tests.common import MockConfigEntry VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" +ROOM_LIST = ["Kitchen", "Living Room"] EXPECTED_FEATURES = ( VacuumEntityFeature.BATTERY | VacuumEntityFeature.FAN_SPEED @@ -129,6 +134,10 @@ class MockShark(SharkIqVacuum): """Set a property locally without hitting the API.""" self.set_property_value(property_name, value) + def get_room_list(self): + """Return the list of available rooms without hitting the API.""" + return ROOM_LIST + @pytest.fixture(autouse=True) @patch("sharkiq.ayla_api.AylaApi", MockAyla) @@ -165,6 +174,7 @@ async def test_simple_properties(hass: HomeAssistant) -> None: (ATTR_ERROR_MSG, "Cliff sensor is blocked"), (ATTR_LOW_LIGHT, False), (ATTR_RECHARGE_RESUME, True), + (ATTR_ROOMS, ROOM_LIST), ], ) async def test_initial_attributes( @@ -223,6 +233,24 @@ async def test_device_properties( assert getattr(device, device_property) == target_value +@pytest.mark.parametrize( + ("room_list", "exception"), + [ + (["KITCHEN"], exceptions.ServiceValidationError), + (["KITCHEN", "MUD_ROOM", "DOG HOUSE"], exceptions.ServiceValidationError), + (["Office"], exceptions.ServiceValidationError), + ([], MultipleInvalid), + ], +) +async def test_clean_room_error( + hass: HomeAssistant, room_list: list, exception: Exception +) -> None: + """Test clean_room errors.""" + with pytest.raises(exception): + data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list} + await hass.services.async_call(DOMAIN, SERVICE_CLEAN_ROOM, data, blocking=True) + + async def test_locate(hass: HomeAssistant) -> None: """Test that the locate command works.""" with patch.object(SharkIqVacuum, "async_find_device") as mock_locate: @@ -231,6 +259,18 @@ async def test_locate(hass: HomeAssistant) -> None: mock_locate.assert_called_once() +@pytest.mark.parametrize( + ("room_list"), + [(ROOM_LIST), (["Kitchen"])], +) +async def test_clean_room(hass: HomeAssistant, room_list: list) -> None: + """Test that the clean_room command works.""" + with patch.object(SharkIqVacuum, "async_clean_rooms") as mock_clean_room: + data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list} + await hass.services.async_call(DOMAIN, SERVICE_CLEAN_ROOM, data, blocking=True) + mock_clean_room.assert_called_once_with(room_list) + + @pytest.mark.parametrize( ("side_effect", "success"), [ diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 93b06ddf9d8..526ac1643ec 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -11,7 +11,7 @@ import pytest from homeassistant.components import shell_command from homeassistant.core import HomeAssistant -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.setup import async_setup_component @@ -199,7 +199,10 @@ async def test_non_text_stdout_capture( assert not response # Non-text output throws with 'return_response' - with pytest.raises(UnicodeDecodeError): + with pytest.raises( + HomeAssistantError, + match="Unable to handle non-utf8 output of command: `curl -o - https://raw.githubusercontent.com/home-assistant/assets/master/misc/loading-screen.gif`", + ): response = await hass.services.async_call( "shell_command", "output_image", blocking=True, return_response=True ) @@ -258,7 +261,10 @@ async def test_do_not_run_forever( side_effect=mock_create_subprocess_shell, ), ): - with pytest.raises(TimeoutError): + with pytest.raises( + HomeAssistantError, + match="Timed out running command: `mock_sleep 10000`, after: 0.001 seconds", + ): await hass.services.async_call( shell_command.DOMAIN, "test_service", diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 3cd27101f76..18813ff7eba 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -319,6 +319,11 @@ async def mock_block_device(): {}, BlockUpdateType.COAP_REPLY ) + def online(): + block_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, BlockUpdateType.ONLINE + ) + device = Mock( spec=BlockDevice, blocks=MOCK_BLOCKS, @@ -337,6 +342,7 @@ async def mock_block_device(): block_device_mock.return_value.mock_update_reply = Mock( side_effect=update_reply ) + block_device_mock.return_value.mock_online = Mock(side_effect=online) yield block_device_mock.return_value @@ -376,16 +382,28 @@ async def mock_rpc_device(): {}, RpcUpdateType.EVENT ) + def online(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.ONLINE + ) + def disconnected(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( {}, RpcUpdateType.DISCONNECTED ) + def initialized(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.INITIALIZED + ) + device = _mock_rpc_device() rpc_device_mock.return_value = device rpc_device_mock.return_value.mock_disconnected = Mock(side_effect=disconnected) rpc_device_mock.return_value.mock_update = Mock(side_effect=update) rpc_device_mock.return_value.mock_event = Mock(side_effect=event) + rpc_device_mock.return_value.mock_online = Mock(side_effect=online) + rpc_device_mock.return_value.mock_initialized = Mock(side_effect=initialized) yield rpc_device_mock.return_value diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 00a430cd4b1..524bc1e8ffc 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -144,8 +144,8 @@ async def test_block_sleeping_binary_sensor( assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -180,8 +180,8 @@ async def test_block_restored_sleeping_binary_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -206,8 +206,8 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -263,6 +263,7 @@ async def test_rpc_sleeping_binary_sensor( ) -> None: """Test RPC online sleeping binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) config_entry = await init_integration(hass, 2, sleep_period=1000) # Sensor should be created when device is online @@ -273,8 +274,8 @@ async def test_rpc_sleeping_binary_sensor( ) # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -344,6 +345,10 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 7e0e2d1ce46..a70cdef3fb1 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -25,7 +25,12 @@ from homeassistant.components.climate import ( from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceRegistry @@ -64,8 +69,8 @@ async def test_climate_hvac_mode( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) # Test initial hvac mode - off state = hass.states.get(ENTITY_ID) @@ -125,8 +130,8 @@ async def test_climate_set_temperature( await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -192,8 +197,8 @@ async def test_climate_set_preset_mode( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE @@ -278,8 +283,8 @@ async def test_block_restored_climate( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == HVACMode.OFF assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 @@ -349,8 +354,8 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device, "initialized", True) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == HVACMode.OFF assert hass.states.get(entity_id).attributes.get("temperature") == 39 @@ -451,8 +456,8 @@ async def test_block_set_mode_connection_error( await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -476,10 +481,10 @@ async def test_block_set_mode_auth_error( entry = await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( CLIMATE_DOMAIN, @@ -489,7 +494,7 @@ async def test_block_set_mode_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -527,17 +532,17 @@ async def test_block_restored_climate_auth_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Make device online with auth error monkeypatch.setattr(mock_block_device, "initialized", True) type(mock_block_device).settings = PropertyMock( return_value={}, side_effect=InvalidAuthError ) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -561,8 +566,8 @@ async def test_device_not_calibrated( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) mock_status = MOCK_STATUS_COAP.copy() mock_status["calibrated"] = False @@ -711,3 +716,36 @@ async def test_wall_display_thermostat_mode( entry = entity_registry.async_get(climate_entity_id) assert entry assert entry.unique_id == "123456789ABC-thermostat:0" + + +async def test_wall_display_thermostat_mode_external_actuator( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Wall Display in thermostat mode with an external actuator.""" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_switch_0" + + new_status = deepcopy(mock_rpc_device.status) + new_status["sys"]["relay_in_thermostat"] = False + monkeypatch.setattr(mock_rpc_device, "status", new_status) + + await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) + + # the switch entity should be created + state = hass.states.get(switch_entity_id) + assert state + assert state.state == STATE_ON + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + # the climate entity should be created + state = hass.states.get(climate_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 + + entry = entity_registry.async_get(climate_entity_id) + assert entry + assert entry.unique_id == "123456789ABC-thermostat:0" diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1e7bbc01d6d..c73b93f9fdb 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -10,12 +10,11 @@ from aioshelly.const import DEFAULT_HTTP_PORT, MODEL_1, MODEL_PLUS_2PM from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, ) import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.shelly import config_flow from homeassistant.components.shelly.const import ( @@ -26,6 +25,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.components.shelly.coordinator import ENTRY_RELOAD_COOLDOWN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -74,7 +74,7 @@ async def test_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["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -102,7 +102,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -123,7 +123,7 @@ async def test_form_gen1_custom_port( 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["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -141,7 +141,7 @@ async def test_form_gen1_custom_port( {"host": "1.1.1.1", "port": "1100"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "custom_port_not_supported" @@ -181,7 +181,7 @@ async def test_form_auth( 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["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -193,7 +193,7 @@ async def test_form_auth( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -210,7 +210,7 @@ async def test_form_auth( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -246,7 +246,7 @@ async def test_form_errors_get_info( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -267,7 +267,7 @@ async def test_form_missing_model_key( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -278,7 +278,7 @@ async def test_form_missing_model_key_auth_enabled( 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["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -290,14 +290,14 @@ async def test_form_missing_model_key_auth_enabled( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result["errors"] == {} monkeypatch.setattr(mock_rpc_device, "shelly", {"gen": 2}) result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "1234"} ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -317,14 +317,14 @@ async def test_form_missing_model_key_zeroconf( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "firmware_not_fully_provisioned"} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -357,7 +357,7 @@ async def test_form_errors_test_connection( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -382,7 +382,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -424,7 +424,7 @@ async def test_user_setup_ignored_device( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" @@ -432,25 +432,6 @@ async def test_user_setup_ignored_device( assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_firmware_unsupported(hass: HomeAssistant) -> None: - """Test we abort if device firmware is unsupported.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.shelly.config_flow.get_info", - side_effect=FirmwareUnsupported, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "1.1.1.1"}, - ) - - assert result2["type"] == data_entry_flow.FlowResultType.ABORT - assert result2["reason"] == "unsupported_firmware" - - @pytest.mark.parametrize( ("exc", "base_error"), [ @@ -484,7 +465,7 @@ async def test_form_auth_errors_test_connection_gen1( result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -520,7 +501,7 @@ async def test_form_auth_errors_test_connection_gen2( result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test password"} ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -562,7 +543,7 @@ async def test_zeroconf( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -586,7 +567,7 @@ async def test_zeroconf( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -621,7 +602,7 @@ async def test_zeroconf_sleeping_device( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -644,7 +625,7 @@ async def test_zeroconf_sleeping_device( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -678,7 +659,7 @@ async def test_zeroconf_sleeping_device_error(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -699,7 +680,7 @@ async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -726,7 +707,7 @@ async def test_zeroconf_ignored(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -749,29 +730,13 @@ async def test_zeroconf_with_wifi_ap_ip(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry was not updated with the wifi ap ip assert entry.data["host"] == "2.2.2.2" -async def test_zeroconf_firmware_unsupported(hass: HomeAssistant) -> None: - """Test we abort if device firmware is unsupported.""" - with patch( - "homeassistant.components.shelly.config_flow.get_info", - side_effect=FirmwareUnsupported, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=DISCOVERY_INFO, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "unsupported_firmware" - - async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: """Test we get the form.""" with patch( @@ -783,7 +748,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -801,7 +766,7 @@ async def test_zeroconf_require_auth( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -819,7 +784,7 @@ async def test_zeroconf_require_auth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -865,7 +830,7 @@ async def test_reauth_successful( data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -873,7 +838,7 @@ async def test_reauth_successful( user_input=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -914,7 +879,7 @@ async def test_reauth_unsuccessful( data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -922,15 +887,11 @@ async def test_reauth_unsuccessful( user_input=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" -@pytest.mark.parametrize( - "error", - [DeviceConnectionError, FirmwareUnsupported], -) -async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> None: +async def test_reauth_get_info_error(hass: HomeAssistant) -> None: """Test reauthentication flow failed with error in get_info().""" entry = MockConfigEntry( domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": 2} @@ -939,7 +900,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N with patch( "homeassistant.components.shelly.config_flow.get_info", - side_effect=error, + side_effect=DeviceConnectionError, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -947,7 +908,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -955,7 +916,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N user_input={"password": "test2 password"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -1026,7 +987,7 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N """Test setting ble options for gen2 devices.""" entry = await init_integration(hass, 2) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -1038,11 +999,11 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.DISABLED result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -1054,11 +1015,11 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.ACTIVE result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -1070,7 +1031,7 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.PASSIVE await hass.config_entries.async_unload(entry.entry_id) @@ -1099,7 +1060,7 @@ async def test_zeroconf_already_configured_triggers_refresh_mac_in_name( data=DISCOVERY_INFO_WITH_MAC, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -1134,7 +1095,7 @@ async def test_zeroconf_already_configured_triggers_refresh( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -1153,6 +1114,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( caplog: pytest.LogCaptureFixture, ) -> None: """Test zeroconf discovery does not triggers refresh for sleeping device.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = MockConfigEntry( domain="shelly", unique_id="AABBCCDDEEFF", @@ -1162,10 +1124,11 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert "online, resuming setup" in caplog.text + assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( "homeassistant.components.shelly.config_flow.get_info", @@ -1176,7 +1139,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -1185,7 +1148,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( 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 len(mock_rpc_device.initialize.mock_calls) == 1 assert "device did not update" not in caplog.text @@ -1197,7 +1160,7 @@ 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"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index c16f78b83ff..1dc45a98c44 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1,14 +1,10 @@ """Tests for Shelly coordinator.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, call, patch from aioshelly.const import MODEL_BULB, MODEL_BUTTON1 -from aioshelly.exceptions import ( - DeviceConnectionError, - FirmwareUnsupported, - InvalidAuthError, -) +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from freezegun.api import FrozenDateTimeFactory import pytest @@ -28,14 +24,15 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, State +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, + DeviceRegistry, async_entries_for_config_entry, async_get as async_get_dev_reg, format_mac, ) -import homeassistant.helpers.issue_registry as ir from . import ( MOCK_MAC, @@ -44,10 +41,11 @@ from . import ( inject_rpc_device_event, mock_polling_rpc_update, mock_rest_update, + register_device, register_entity, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 @@ -159,14 +157,14 @@ async def test_block_polling_auth_error( ) entry = await init_integration(hass, 1) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Move time to generate polling freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -198,11 +196,11 @@ async def test_block_rest_update_auth_error( AsyncMock(side_effect=InvalidAuthError), ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await mock_rest_update(hass, freezer) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -216,28 +214,25 @@ async def test_block_rest_update_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -async def test_block_firmware_unsupported( +async def test_block_sleeping_device_firmware_unsupported( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, ) -> None: - """Test block device polling authentication error.""" - monkeypatch.setattr( - mock_block_device, - "update", - AsyncMock(side_effect=FirmwareUnsupported), - ) - entry = await init_integration(hass, 1) - - assert entry.state is ConfigEntryState.LOADED - - # Move time to generate polling - freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) - async_fire_time_changed(hass) - await hass.async_block_till_done() + """Test block sleeping device firmware not supported.""" + monkeypatch.setattr(mock_block_device, "firmware_supported", False) + entry = await init_integration(hass, 1, sleep_period=3600) + + # Make device online + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues async def test_block_polling_connection_error( @@ -290,20 +285,28 @@ async def test_block_rest_update_connection_error( async def test_block_sleeping_device_no_periodic_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) == "22.1" # Move time to generate polling - freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 3600)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -352,7 +355,7 @@ async def test_block_button_click_event( entry = await init_integration(hass, 1, model=MODEL_BUTTON1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() dev_reg = async_get_dev_reg(hass) @@ -468,7 +471,7 @@ async def test_rpc_reload_with_invalid_auth( async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -529,6 +532,7 @@ async def test_rpc_update_entry_sleep_period( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC update entry sleep period.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) register_entity( hass, @@ -539,8 +543,8 @@ async def test_rpc_update_entry_sleep_period( ) # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.data["sleep_period"] == 600 @@ -548,16 +552,20 @@ async def test_rpc_update_entry_sleep_period( monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.data["sleep_period"] == 3600 async def test_rpc_sleeping_device_no_periodic_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) register_entity( hass, @@ -568,38 +576,38 @@ async def test_rpc_sleeping_device_no_periodic_updates( ) # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) is STATE_UNAVAILABLE -async def test_rpc_firmware_unsupported( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock +async def test_rpc_sleeping_device_firmware_unsupported( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, ) -> None: - """Test RPC update entry unsupported firmware.""" - entry = await init_integration(hass, 2) - register_entity( - hass, - SENSOR_DOMAIN, - "test_name_temperature", - "temperature:0-temperature_0", - entry, - ) + """Test RPC sleeping device firmware not supported.""" + monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) + entry = await init_integration(hass, 2, sleep_period=3600) - # Move time to generate sleep period update - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) - async_fire_time_changed(hass) - await hass.async_block_till_done() + # Make device online + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues async def test_rpc_reconnect_auth_error( @@ -620,14 +628,14 @@ async def test_rpc_reconnect_auth_error( ), ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Move time to generate reconnect freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -659,11 +667,11 @@ async def test_rpc_polling_auth_error( ), ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await mock_polling_rpc_update(hass, freezer) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -753,12 +761,13 @@ async def test_rpc_update_entry_fw_ver( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test RPC update entry firmware version.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) dev_reg = async_get_dev_reg(hass) # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.unique_id device = dev_reg.async_get_device( @@ -779,3 +788,113 @@ async def test_rpc_update_entry_fw_ver( ) assert device assert device.sw_version == "99.0.0" + + +async def test_rpc_runs_connected_events_when_initialized( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC runs connected events when initialized.""" + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await init_integration(hass, 2) + + assert call.script_list() not in mock_rpc_device.mock_calls + + # Mock initialized event + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done() + + # BLE script list is called during connected events + assert call.script_list() in mock_rpc_device.mock_calls + + +async def test_block_sleeping_device_connection_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + device_reg: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test block sleeping device connection error during initialize.""" + sleep_period = 1000 + entry = await init_integration(hass, 1, sleep_period=sleep_period, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_block_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_ON + + # Move time to generate sleep period update + freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE + + +async def test_rpc_sleeping_device_connection_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + device_reg: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC sleeping device connection error during initialize.""" + sleep_period = 1000 + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_rpc_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_ON + + # Move time to generate sleep period update + freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index c4db8acaf6d..39238f1674a 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -96,11 +96,11 @@ async def test_get_triggers_rpc_device( CONF_PLATFORM: "device", CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, - CONF_TYPE: type, + CONF_TYPE: trigger_type, CONF_SUBTYPE: "button1", "metadata": {}, } - for type in [ + for trigger_type in [ "btn_down", "btn_up", "single_push", @@ -130,11 +130,11 @@ async def test_get_triggers_button( CONF_PLATFORM: "device", CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, - CONF_TYPE: type, + CONF_TYPE: trigger_type, CONF_SUBTYPE: "button", "metadata": {}, } - for type in ["single", "double", "triple", "long"] + for trigger_type in ["single", "double", "triple", "long"] ] triggers = await async_get_device_automations( diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 754f1111548..61ec8ce6779 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -8,7 +8,6 @@ from aioshelly.common import ConnectionOptions from aioshelly.const import MODEL_PLUS_2PM from aioshelly.exceptions import ( DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, MacAddressMismatchError, ) @@ -27,6 +26,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceRegistry, @@ -145,25 +145,44 @@ async def test_setup_entry_not_shelly( @pytest.mark.parametrize("gen", [1, 2, 3]) -@pytest.mark.parametrize("side_effect", [DeviceConnectionError, FirmwareUnsupported]) async def test_device_connection_error( hass: HomeAssistant, gen: int, - side_effect: Exception, mock_block_device: Mock, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test device connection error.""" monkeypatch.setattr( - mock_block_device, "initialize", AsyncMock(side_effect=side_effect) + mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError) ) monkeypatch.setattr( - mock_rpc_device, "initialize", AsyncMock(side_effect=side_effect) + mock_rpc_device, "initialize", AsyncMock(side_effect=DeviceConnectionError) ) entry = await init_integration(hass, gen) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize("gen", [1, 2, 3]) +async def test_device_unsupported_firmware( + hass: HomeAssistant, + gen: int, + mock_block_device: Mock, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, +) -> None: + """Test device init with unsupported firmware.""" + monkeypatch.setattr(mock_block_device, "firmware_supported", False) + monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) + + entry = await init_integration(hass, gen) + assert entry.state is ConfigEntryState.SETUP_RETRY + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues @pytest.mark.parametrize("gen", [1, 2, 3]) @@ -183,7 +202,7 @@ async def test_mac_mismatch_error( ) entry = await init_integration(hass, gen) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize("gen", [1, 2, 3]) @@ -203,7 +222,7 @@ async def test_device_auth_error( ) entry = await init_integration(hass, gen) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -217,12 +236,13 @@ async def test_device_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -@pytest.mark.parametrize(("entry_sleep", "device_sleep"), [(None, 0), (1000, 1000)]) +@pytest.mark.parametrize(("entry_sleep", "device_sleep"), [(None, 0), (3600, 3600)]) async def test_sleeping_block_device_online( hass: HomeAssistant, entry_sleep: int | None, device_sleep: int, mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, device_reg: DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: @@ -234,10 +254,17 @@ async def test_sleeping_block_device_online( connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": int(device_sleep / 60), "unit": "m"}, + ) entry = await init_integration(hass, 1, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text - mock_block_device.mock_update() + mock_block_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -248,13 +275,17 @@ async def test_sleeping_rpc_device_online( entry_sleep: int | None, device_sleep: int, mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test sleeping RPC device online.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", device_sleep) entry = await init_integration(hass, 2, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -270,7 +301,9 @@ async def test_sleeping_rpc_device_online_new_firmware( assert "will resume when device is online" in caplog.text mutate_rpc_device_status(monkeypatch, mock_rpc_device, "sys", "wakeup_period", 1500) - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == 1500 @@ -413,9 +446,12 @@ async def test_entry_missing_port(hass: HomeAssistant) -> None: } 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: + with ( + patch("homeassistant.components.shelly.RpcDevice.initialize"), + 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() @@ -435,9 +471,12 @@ async def test_rpc_entry_custom_port(hass: HomeAssistant) -> None: } 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: + with ( + patch("homeassistant.components.shelly.RpcDevice.initialize"), + 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() diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index ecc6d7410bf..0b9fee9e47f 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -33,13 +33,18 @@ async def test_block_number_update( ) -> None: """Test block device number update.""" entity_id = "number.test_name_valve_position" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -93,8 +98,8 @@ async def test_block_restored_number( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -130,21 +135,28 @@ async def test_block_restored_number_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" async def test_block_number_set_value( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device number set value.""" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) mock_block_device.reset_mock() await hass.services.async_call( @@ -162,15 +174,20 @@ async def test_block_set_value_connection_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set value connection error.""" + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) monkeypatch.setattr( mock_block_device, "http_request", AsyncMock(side_effect=DeviceConnectionError), ) - await init_integration(hass, 1, sleep_period=1000) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() with pytest.raises(HomeAssistantError): @@ -186,18 +203,23 @@ async def test_block_set_value_auth_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set value authentication error.""" + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) monkeypatch.setattr( mock_block_device, "http_request", AsyncMock(side_effect=InvalidAuthError), ) - entry = await init_integration(hass, 1, sleep_period=1000) + entry = await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( NUMBER_DOMAIN, @@ -207,7 +229,7 @@ async def test_block_set_value_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 0a15b78994b..ceaa9b66b8d 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -164,8 +164,8 @@ async def test_block_sleeping_sensor( assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -206,8 +206,8 @@ async def test_block_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -232,8 +232,8 @@ async def test_block_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -305,8 +305,8 @@ async def test_block_not_matched_restored_sleeping_sensor( mock_block_device.blocks[SENSOR_BLOCK_ID], "description", "other_desc" ) monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "20.4" @@ -448,6 +448,7 @@ async def test_rpc_sleeping_sensor( ) -> None: """Test RPC online sleeping sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) # Sensor should be created when device is online @@ -462,8 +463,8 @@ async def test_rpc_sleeping_sensor( ) # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.9" @@ -501,6 +502,10 @@ async def test_rpc_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -533,6 +538,10 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -583,20 +592,22 @@ async def test_rpc_sleeping_update_entity_service( hass: HomeAssistant, mock_rpc_device: Mock, entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) # Entity should be created when device is online assert hass.states.get(entity_id) is None # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == "22.9" @@ -627,20 +638,26 @@ async def test_block_sleeping_update_entity_service( hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test block sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Sensor should be created when device is online assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() - await hass.async_block_till_done() + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index a57a9890921..dd214c8841d 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -98,7 +98,7 @@ async def test_block_set_state_auth_error( ) entry = await init_integration(hass, 1) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( SWITCH_DOMAIN, @@ -108,7 +108,7 @@ async def test_block_set_state_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -242,7 +242,7 @@ async def test_rpc_auth_error( ) entry = await init_integration(hass, 2) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( SWITCH_DOMAIN, @@ -252,7 +252,7 @@ async def test_rpc_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -330,6 +330,7 @@ async def test_wall_display_relay_mode( new_status = deepcopy(mock_rpc_device.status) new_status["sys"]["relay_in_thermostat"] = False + new_status.pop("thermostat:0") monkeypatch.setattr(mock_rpc_device, "status", new_status) await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index f3960620a21..0f26fd14d12 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -199,7 +199,7 @@ async def test_block_update_auth_error( ) entry = await init_integration(hass, 1) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( UPDATE_DOMAIN, @@ -209,7 +209,7 @@ async def test_block_update_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -335,6 +335,7 @@ async def test_rpc_sleeping_update( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device update entity.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( mock_rpc_device.status["sys"], @@ -350,8 +351,8 @@ async def test_rpc_sleeping_update( assert hass.states.get(entity_id) is None # Make device online - mock_rpc_device.mock_update() - await hass.async_block_till_done() + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -411,6 +412,10 @@ async def test_rpc_restored_sleeping_update( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -456,6 +461,10 @@ async def test_rpc_restored_sleeping_update_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -651,7 +660,7 @@ async def test_rpc_update_auth_error( ) entry = await init_integration(hass, 2) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( UPDATE_DOMAIN, @@ -661,7 +670,7 @@ async def test_rpc_update_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index 1d807e87ca2..4f6f5270c08 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -12,7 +12,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_user(hass: HomeAssistant) -> None: @@ -22,7 +22,7 @@ async def test_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -33,7 +33,7 @@ async def test_user_confirm(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == {} @@ -43,6 +43,6 @@ async def test_onboarding_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Shopping list" assert result["data"] == {} diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index fb6f61d4edf..173544d0be2 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -53,8 +53,7 @@ async def ws_move_item( if previous_uid is not None: data["previous_uid"] = previous_uid await client.send_json_auto_id(data) - resp = await client.receive_json() - return resp + return await client.receive_json() return move diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 542c06da24f..36f2292bdea 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sia.config_flow import ACCOUNT_SCHEMA, HUB_SCHEMA from homeassistant.components.sia.const import ( CONF_ACCOUNT, @@ -18,6 +18,7 @@ from homeassistant.components.sia.const import ( ) from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -164,9 +165,7 @@ async def test_form_start_account( async def test_create(hass: HomeAssistant, entry_with_basic_config) -> None: """Test we create a entry through the form.""" - assert ( - entry_with_basic_config["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - ) + assert entry_with_basic_config["type"] is FlowResultType.CREATE_ENTRY assert ( entry_with_basic_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -179,10 +178,7 @@ async def test_create_additional_account( hass: HomeAssistant, entry_with_additional_account_config ) -> None: """Test we create a config with two accounts.""" - assert ( - entry_with_additional_account_config["type"] - == data_entry_flow.FlowResultType.CREATE_ENTRY - ) + assert entry_with_additional_account_config["type"] is FlowResultType.CREATE_ENTRY assert ( entry_with_additional_account_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -209,7 +205,7 @@ async def test_abort_form(hass: HomeAssistant) -> None: get_abort = await hass.config_entries.flow.async_configure( start_another_flow["flow_id"], BASIC_CONFIG ) - assert get_abort["type"] == "abort" + assert get_abort["type"] is FlowResultType.ABORT assert get_abort["reason"] == "already_configured" @@ -243,7 +239,7 @@ async def test_validation_errors_user( flow_id = flow_at_user_step["flow_id"] config[field] = value result_err = await hass.config_entries.flow.async_configure(flow_id, config) - assert result_err["type"] == "form" + assert result_err["type"] is FlowResultType.FORM assert result_err["errors"] == {"base": error} @@ -273,7 +269,7 @@ async def test_validation_errors_account( flow_id = flow_at_add_account_step["flow_id"] config[field] = value result_err = await hass.config_entries.flow.async_configure(flow_id, config) - assert result_err["type"] == "form" + assert result_err["type"] is FlowResultType.FORM assert result_err["errors"] == {"base": error} @@ -322,7 +318,7 @@ async def test_options_basic(hass: HomeAssistant) -> None: ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" assert result["last_step"] @@ -330,7 +326,7 @@ async def test_options_basic(hass: HomeAssistant) -> None: result["flow_id"], BASIC_OPTIONS ) await hass.async_block_till_done() - assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert updated["type"] is FlowResultType.CREATE_ENTRY assert updated["data"] == { CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} } @@ -348,13 +344,13 @@ async def test_options_additional(hass: HomeAssistant) -> None: ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" assert not result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.FlowResultType.FORM + assert updated["type"] is FlowResultType.FORM assert updated["step_id"] == "options" assert updated["last_step"] diff --git a/tests/components/signal_messenger/conftest.py b/tests/components/signal_messenger/conftest.py index ecafff1ef4a..1c9c60c2878 100644 --- a/tests/components/signal_messenger/conftest.py +++ b/tests/components/signal_messenger/conftest.py @@ -32,7 +32,7 @@ def signal_requests_mock_factory(requests_mock: Mocker) -> Mocker: """Create signal service mock from factory.""" def _signal_requests_mock_factory( - success_send_result: bool = True, content_length_header: str = None + success_send_result: bool = True, content_length_header: str | None = None ) -> Mocker: requests_mock.register_uri( "GET", diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 3905014747b..6718491f2a0 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch import pytest from simplepush import UnknownError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.simplepush.const import CONF_DEVICE_KEY, CONF_SALT, DOMAIN from homeassistant.const import CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG @@ -60,7 +61,7 @@ async def test_flow_with_password(hass: HomeAssistant) -> None: result["flow_id"], user_input=mock_config_pass, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == mock_config_pass @@ -83,7 +84,7 @@ async def test_flow_user_device_key_already_configured(hass: HomeAssistant) -> N result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -108,7 +109,7 @@ async def test_flow_user_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -126,5 +127,5 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index af92833eb5b..dde7e37b891 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -27,12 +27,12 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -42,12 +42,12 @@ async def test_invalid_auth_code_length(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} @@ -61,13 +61,13 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} @@ -79,14 +79,14 @@ async def test_options_flow(config_entry, hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CODE: "4321"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} @@ -108,7 +108,7 @@ async def test_step_reauth(config_entry, hass: HomeAssistant, setup_simplisafe) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -137,7 +137,7 @@ async def test_step_reauth_wrong_account( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -178,7 +178,7 @@ async def test_step_user( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY if log_statement: assert any(m for m in caplog.messages if log_statement in m) @@ -198,10 +198,10 @@ async def test_unknown_error(hass: HomeAssistant, setup_simplisafe) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 535eb91f01b..cb62f808efc 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -33,7 +33,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -41,7 +41,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_DATA assert result["result"].unique_id == USER_ID @@ -59,7 +59,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -69,7 +69,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, skybell_mock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +83,7 @@ async def test_invalid_credentials(hass: HomeAssistant, skybell_mock) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -94,7 +94,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, skybell_mock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -114,14 +114,14 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -140,7 +140,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" skybell_mock.async_initialize.side_effect = ( @@ -151,7 +151,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: user_input={CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} skybell_mock.async_initialize.side_effect = None @@ -160,5 +160,5 @@ async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: result["flow_id"], user_input={CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index c7b8d927c94..565b5ec2149 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.slack.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA, CONF_INPUT, TEAM_NAME, create_entry, mock_connection @@ -24,7 +25,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -43,7 +44,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -57,7 +58,7 @@ async def test_flow_user_invalid_auth( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -72,7 +73,7 @@ async def test_flow_user_cannot_connect( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -88,6 +89,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/slack/test_init.py b/tests/components/slack/test_init.py index e206e066c67..7f36ec01733 100644 --- a/tests/components/slack/test_init.py +++ b/tests/components/slack/test_init.py @@ -13,7 +13,7 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - """Test Slack setup.""" entry: ConfigEntry = await async_init_integration(hass, aioclient_mock) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -26,7 +26,7 @@ async def test_async_setup_entry_not_ready( hass, aioclient_mock, error="cannot_connect" ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_async_setup_entry_invalid_auth( @@ -37,4 +37,4 @@ async def test_async_setup_entry_invalid_auth( hass, aioclient_mock, error="invalid_auth" ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index b623252cec4..af08f5aa9fe 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import SLEEPIQ_CONFIG, setup_platform @@ -49,7 +50,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -70,7 +71,7 @@ async def test_login_failure(hass: HomeAssistant, side_effect, error) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error} @@ -80,7 +81,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("asyncsleepiq.AsyncSleepIQ.login", return_value=True): @@ -89,7 +90,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 @@ -124,5 +125,5 @@ async def test_reauth_password(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/slimproto/test_config_flow.py b/tests/components/slimproto/test_config_flow.py index 686768c6eb6..97da39517bd 100644 --- a/tests/components/slimproto/test_config_flow.py +++ b/tests/components/slimproto/test_config_flow.py @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == DEFAULT_NAME assert result.get("data") == {} @@ -35,5 +35,5 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index d73d8eb9728..93ac1783e09 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] assert result["data"] == MOCK_USER_INPUT @@ -58,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -78,7 +78,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert len(mock_setup_entry.mock_calls) == 0 @@ -99,7 +99,7 @@ async def test_form_cannot_retrieve_device_info(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve_device_info"} assert len(mock_setup_entry.mock_calls) == 0 @@ -119,7 +119,7 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert len(mock_setup_entry.mock_calls) == 0 @@ -143,6 +143,6 @@ async def test_form_already_configured(hass: HomeAssistant, mock_config_entry) - MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index b5551c03c77..82f5baf952f 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -4,7 +4,7 @@ from http import HTTPStatus from ipaddress import ip_address from unittest.mock import patch -from homeassistant import data_entry_flow, setup +from homeassistant import setup from homeassistant.components import zeroconf from homeassistant.components.smappee.const import ( CONF_SERIALNUMBER, @@ -16,6 +16,7 @@ from homeassistant.components.smappee.const import ( from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -34,7 +35,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_user_host_form(hass: HomeAssistant) -> None: @@ -44,14 +45,14 @@ async def test_show_user_host_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_zeroconf_connection_error_form(hass: HomeAssistant) -> None: @@ -72,14 +73,14 @@ async def test_show_zeroconf_connection_error_form(hass: HomeAssistant) -> None: ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -104,14 +105,14 @@ async def test_show_zeroconf_connection_error_form_next_generation( ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -127,19 +128,19 @@ async def test_connection_error(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_user_local_connection_error(hass: HomeAssistant) -> None: @@ -156,19 +157,19 @@ async def test_user_local_connection_error(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zeroconf_wrong_mdns(hass: HomeAssistant) -> None: @@ -188,7 +189,7 @@ async def test_zeroconf_wrong_mdns(hass: HomeAssistant) -> None: ) assert result["reason"] == "invalid_mdns" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_full_user_wrong_mdns(hass: HomeAssistant) -> None: @@ -212,18 +213,18 @@ async def test_full_user_wrong_mdns(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_mdns" @@ -257,18 +258,18 @@ async def test_user_device_exists_abort(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -312,7 +313,7 @@ async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -333,7 +334,7 @@ async def test_cloud_device_exists_abort(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -362,7 +363,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass: HomeAssistant) -> Non properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -396,7 +397,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -421,7 +422,7 @@ async def test_abort_cloud_flow_if_local_device_exists(hass: HomeAssistant) -> N result["flow_id"], {"environment": ENV_CLOUD} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_local_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -511,7 +512,7 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} @@ -519,7 +520,7 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -549,7 +550,7 @@ async def test_full_user_local_flow(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] is None result = await hass.config_entries.flow.async_configure( @@ -557,12 +558,12 @@ async def test_full_user_local_flow(hass: HomeAssistant) -> None: {"environment": ENV_LOCAL}, ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -596,7 +597,7 @@ async def test_full_zeroconf_flow_next_generation(hass: HomeAssistant) -> None: properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} @@ -604,7 +605,7 @@ async def test_full_zeroconf_flow_next_generation(hass: HomeAssistant) -> None: result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "smappee5001000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 04a3344b5cc..d06571fe05e 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -58,7 +58,7 @@ def mock_connection( """Mock all calls to the API.""" aioclient_mock.get(BASE_URL) - auth_endpoint = f"{BASE_ENDPOINT}{AUTH_ENDPOINT}" + auth_endpoint = AUTH_ENDPOINT if not auth_fail and not auth_timeout: aioclient_mock.post( auth_endpoint, diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index 53f7a2eb5fd..a98597686d5 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant import config_entries from homeassistant.components.smart_meter_texas.const import DOMAIN 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 @@ -25,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_LOGIN[CONF_USERNAME] assert result2["data"] == TEST_LOGIN assert len(mock_setup_entry.mock_calls) == 1 @@ -61,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_LOGIN, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, side_effect) -> None: result["flow_id"], TEST_LOGIN ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +102,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: TEST_LOGIN, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -123,5 +124,5 @@ async def test_form_duplicate_account(hass: HomeAssistant) -> None: data={"username": "user123", "password": "password123"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index f15ba85c07e..d25cc8849e5 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -253,7 +253,7 @@ def device_factory_fixture(): api = Mock(Api) api.post_device_command.return_value = {"results": [{"status": "ACCEPTED"}]} - def _factory(label, capabilities, status: dict = None): + def _factory(label, capabilities, status: dict | None = None): device_data = { "deviceId": str(uuid4()), "name": "Device Type Handler Name", @@ -342,7 +342,7 @@ def event_request_factory_fixture(event_factory): if events is None: events = [] if device_ids: - events.extend([event_factory(id) for id in device_ids]) + events.extend([event_factory(device_id) for device_id in device_ids]) events.append(event_factory(uuid4())) events.append(event_factory(device_ids[0], event_type="OTHER")) request.events = events diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index e3dcf76bbaf..49444e47780 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -8,7 +8,7 @@ from aiohttp import ClientResponseError from pysmartthings import APIResponseError from pysmartthings.installedapp import format_install_url -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.const import ( CONF_APP_ID, @@ -19,6 +19,7 @@ from homeassistant.components.smartthings.const import ( from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_import_shows_user_step(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -56,7 +57,7 @@ async def test_entry_created( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -64,7 +65,7 @@ async def test_entry_created( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -73,14 +74,14 @@ async def test_entry_created( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -89,7 +90,7 @@ async def test_entry_created( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -127,7 +128,7 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -135,7 +136,7 @@ async def test_entry_created_from_update_event( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -144,14 +145,14 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -160,7 +161,7 @@ async def test_entry_created_from_update_event( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -199,7 +200,7 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -207,7 +208,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -216,14 +217,14 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -232,7 +233,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -283,7 +284,7 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -291,7 +292,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -302,14 +303,14 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -318,7 +319,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -377,7 +378,7 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -387,7 +388,7 @@ async def test_entry_created_with_cloudhook( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -396,14 +397,14 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -412,7 +413,7 @@ async def test_entry_created_with_cloudhook( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -440,7 +441,7 @@ async def test_invalid_webhook_aborts(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_webhook_url" assert result["description_placeholders"][ "webhook_url" @@ -456,7 +457,7 @@ async def test_invalid_token_shows_error(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -464,7 +465,7 @@ async def test_invalid_token_shows_error(hass: HomeAssistant) -> None: # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -473,7 +474,7 @@ async def test_invalid_token_shows_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_invalid_format"} @@ -495,7 +496,7 @@ async def test_unauthorized_token_shows_error( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -503,7 +504,7 @@ async def test_unauthorized_token_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -512,7 +513,7 @@ async def test_unauthorized_token_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"} @@ -534,7 +535,7 @@ async def test_forbidden_token_shows_error( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -542,7 +543,7 @@ async def test_forbidden_token_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -551,7 +552,7 @@ async def test_forbidden_token_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"} @@ -579,7 +580,7 @@ async def test_webhook_problem_shows_error( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -587,7 +588,7 @@ async def test_webhook_problem_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -596,7 +597,7 @@ async def test_webhook_problem_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "webhook_error"} @@ -621,7 +622,7 @@ async def test_api_error_shows_error(hass: HomeAssistant, smartthings_mock) -> N 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -629,7 +630,7 @@ async def test_api_error_shows_error(hass: HomeAssistant, smartthings_mock) -> N # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -638,7 +639,7 @@ async def test_api_error_shows_error(hass: HomeAssistant, smartthings_mock) -> N result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -661,7 +662,7 @@ async def test_unknown_response_error_shows_error( 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -669,7 +670,7 @@ async def test_unknown_response_error_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -678,7 +679,7 @@ async def test_unknown_response_error_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -695,7 +696,7 @@ async def test_unknown_error_shows_error(hass: HomeAssistant, smartthings_mock) 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -703,7 +704,7 @@ async def test_unknown_error_shows_error(hass: HomeAssistant, smartthings_mock) # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -712,7 +713,7 @@ async def test_unknown_error_shows_error(hass: HomeAssistant, smartthings_mock) result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -737,7 +738,7 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -745,7 +746,7 @@ async def test_no_available_locations_aborts( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -754,5 +755,5 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_available_locations" diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index c4f6c15a3fe..e19ac403e5d 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -140,7 +140,7 @@ async def test_set_cover_position_switch_level( assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - assert device._api.post_device_command.call_count == 1 # type: ignore + assert device._api.post_device_command.call_count == 1 async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: @@ -171,7 +171,7 @@ async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - assert device._api.post_device_command.call_count == 1 # type: ignore + assert device._api.post_device_command.call_count == 1 async def test_set_cover_position_unsupported( @@ -196,7 +196,7 @@ async def test_set_cover_position_unsupported( # Ensure API was not called - assert device._api.post_device_command.call_count == 0 # type: ignore + assert device._api.post_device_command.call_count == 0 async def test_update_to_open_from_signal(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 6ff640e012a..ae8a288e3a5 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -370,9 +370,9 @@ async def test_remove_entry_installedapp_unknown_error( ) -> None: """Test raises exceptions removing the installed app.""" # Arrange - smartthings_mock.delete_installed_app.side_effect = Exception + smartthings_mock.delete_installed_app.side_effect = ValueError # Act - with pytest.raises(Exception): + with pytest.raises(ValueError): await smartthings.async_remove_entry(hass, config_entry) # Assert assert smartthings_mock.delete_installed_app.call_count == 1 @@ -403,9 +403,9 @@ async def test_remove_entry_app_unknown_error( ) -> None: """Test raises exceptions removing the app.""" # Arrange - smartthings_mock.delete_app.side_effect = Exception + smartthings_mock.delete_app.side_effect = ValueError # Act - with pytest.raises(Exception): + with pytest.raises(ValueError): 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_scene.py b/tests/components/smartthings/test_scene.py index 1eaaad55d0f..d33db0a1dd9 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -38,7 +38,7 @@ async def test_scene_activate(hass: HomeAssistant, scene) -> None: assert state.attributes["icon"] == scene.icon assert state.attributes["color"] == scene.color assert state.attributes["location_id"] == scene.location_id - assert scene.execute.call_count == 1 # type: ignore + assert scene.execute.call_count == 1 async def test_unload_config_entry(hass: HomeAssistant, scene) -> None: diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index df3695f31af..c625f217405 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from smarttub import LoginFailed -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.smarttub.const import DOMAIN 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 @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert result["data"] == { CONF_EMAIL: "test-email", @@ -52,7 +53,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, smarttub_api) -> None: {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -75,14 +76,14 @@ async def test_reauth_success(hass: HomeAssistant, smarttub_api, account) -> Non data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data[CONF_EMAIL] == "test-email3" assert mock_entry.data[CONF_PASSWORD] == "test-password3" @@ -116,12 +117,12 @@ async def test_reauth_wrong_account(hass: HomeAssistant, smarttub_api, account) data=mock_entry2.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 7d8701eca45..a771bcc1e1d 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Home" assert result2["data"] == { "location": { @@ -86,7 +86,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Weather 1.0 1.0" assert result4["data"] == { "location": { @@ -118,7 +118,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "wrong_location"} # Continue flow with new coordinates @@ -143,7 +143,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Weather 2.0 2.0" assert result3["data"] == { "location": { @@ -187,7 +187,7 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -224,7 +224,7 @@ async def test_reconfigure_flow( "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", @@ -241,7 +241,7 @@ async def test_reconfigure_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "wrong_location"} with ( @@ -265,7 +265,7 @@ async def test_reconfigure_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Home" diff --git a/tests/components/sms/__init__.py b/tests/components/sms/__init__.py new file mode 100644 index 00000000000..09b4b0941fb --- /dev/null +++ b/tests/components/sms/__init__.py @@ -0,0 +1 @@ +"""Tests for SMS integration.""" diff --git a/tests/components/sms/const.py b/tests/components/sms/const.py new file mode 100644 index 00000000000..ae875e6d58e --- /dev/null +++ b/tests/components/sms/const.py @@ -0,0 +1,143 @@ +"""Constants for tests of the SMS component.""" + +import datetime + +SMS_STATUS_SINGLE = { + "SIMUnRead": 0, + "SIMUsed": 1, + "SIMSize": 30, + "PhoneUnRead": 0, + "PhoneUsed": 0, + "PhoneSize": 50, + "TemplatesUsed": 0, +} + +NEXT_SMS_SINGLE = [ + { + "SMSC": { + "Location": 0, + "Name": "", + "Format": "Text", + "Validity": "NA", + "Number": "+358444111111", + "DefaultNumber": "", + }, + "UDH": { + "Type": "NoUDH", + "Text": b"", + "ID8bit": 0, + "ID16bit": 0, + "PartNumber": -1, + "AllParts": 0, + }, + "Folder": 1, + "InboxFolder": 1, + "Memory": "SM", + "Location": 1, + "Name": "", + "Number": "+358444222222", + "Text": "Short message", + "Type": "Deliver", + "Coding": "Default_No_Compression", + "DateTime": datetime.datetime(2024, 3, 23, 20, 15, 37), + "SMSCDateTime": datetime.datetime(2024, 3, 23, 20, 15, 41), + "DeliveryStatus": 0, + "ReplyViaSameSMSC": 0, + "State": "UnRead", + "Class": -1, + "MessageReference": 0, + "ReplaceMessage": 0, + "RejectDuplicates": 0, + "Length": 7, + } +] + +SMS_STATUS_MULTIPLE = { + "SIMUnRead": 0, + "SIMUsed": 2, + "SIMSize": 30, + "PhoneUnRead": 0, + "PhoneUsed": 0, + "PhoneSize": 50, + "TemplatesUsed": 0, +} + +NEXT_SMS_MULTIPLE_1 = [ + { + "SMSC": { + "Location": 0, + "Name": "", + "Format": "Text", + "Validity": "NA", + "Number": "+358444111111", + "DefaultNumber": "", + }, + "UDH": { + "Type": "ConcatenatedMessages", + "Text": b"\x05\x00\x03\x00\x02\x01", + "ID8bit": 0, + "ID16bit": -1, + "PartNumber": 1, + "AllParts": 2, + }, + "Folder": 1, + "InboxFolder": 1, + "Memory": "SM", + "Location": 1, + "Name": "", + "Number": "+358444222222", + "Text": "Longer test again: 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123", + "Type": "Deliver", + "Coding": "Default_No_Compression", + "DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56), + "SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 6), + "DeliveryStatus": 0, + "ReplyViaSameSMSC": 0, + "State": "UnRead", + "Class": -1, + "MessageReference": 0, + "ReplaceMessage": 0, + "RejectDuplicates": 0, + "Length": 153, + } +] + +NEXT_SMS_MULTIPLE_2 = [ + { + "SMSC": { + "Location": 0, + "Name": "", + "Format": "Text", + "Validity": "NA", + "Number": "+358444111111", + "DefaultNumber": "", + }, + "UDH": { + "Type": "ConcatenatedMessages", + "Text": b"\x05\x00\x03\x00\x02\x02", + "ID8bit": 0, + "ID16bit": -1, + "PartNumber": 2, + "AllParts": 2, + }, + "Folder": 1, + "InboxFolder": 1, + "Memory": "SM", + "Location": 2, + "Name": "", + "Number": "+358444222222", + "Text": "4567890123456789012345678901", + "Type": "Deliver", + "Coding": "Default_No_Compression", + "DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56), + "SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 7), + "DeliveryStatus": 0, + "ReplyViaSameSMSC": 0, + "State": "UnRead", + "Class": -1, + "MessageReference": 0, + "ReplaceMessage": 0, + "RejectDuplicates": 0, + "Length": 28, + } +] diff --git a/tests/components/sms/test_gateway.py b/tests/components/sms/test_gateway.py new file mode 100644 index 00000000000..132ba9bc1f3 --- /dev/null +++ b/tests/components/sms/test_gateway.py @@ -0,0 +1,52 @@ +"""Test the SMS Gateway.""" + +from unittest.mock import MagicMock + +from homeassistant.components.sms.gateway import Gateway +from homeassistant.core import HomeAssistant + +from .const import ( + NEXT_SMS_MULTIPLE_1, + NEXT_SMS_MULTIPLE_2, + NEXT_SMS_SINGLE, + SMS_STATUS_MULTIPLE, + SMS_STATUS_SINGLE, +) + + +async def test_get_and_delete_all_sms_single_message(hass: HomeAssistant) -> None: + """Test that a single message produces a list of entries containing the single message.""" + + # Mock the Gammu state_machine + state_machine = MagicMock() + state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_SINGLE) + state_machine.GetNextSMS = MagicMock(return_value=NEXT_SMS_SINGLE) + state_machine.DeleteSMS = MagicMock() + + response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine) + + # Assert the length of the list + assert len(response) == 1 + assert len(response[0]) == 1 + + # Assert the content of the message + assert response[0][0]["Text"] == "Short message" + + +async def test_get_and_delete_all_sms_two_part_message(hass: HomeAssistant) -> None: + """Test that a two-part message produces a list of entries containing one combined message.""" + + state_machine = MagicMock() + state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_MULTIPLE) + state_machine.GetNextSMS = MagicMock( + side_effect=iter([NEXT_SMS_MULTIPLE_1, NEXT_SMS_MULTIPLE_2]) + ) + state_machine.DeleteSMS = MagicMock() + + response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine) + + assert len(response) == 1 + assert len(response[0]) == 2 + + assert response[0][0]["Text"] == NEXT_SMS_MULTIPLE_1[0]["Text"] + assert response[0][1]["Text"] == NEXT_SMS_MULTIPLE_2[0]["Text"] diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index b27a7c2d863..15be7b66d27 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest from homeassistant import config as hass_config -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.smtp.const import DOMAIN from homeassistant.components.smtp.notify import MailNotificationService from homeassistant.const import SERVICE_RELOAD @@ -72,7 +72,7 @@ async def test_reload_notify(hass: HomeAssistant) -> None: @pytest.fixture def message(): """Return MockSMTP object with test data.""" - mailer = MockSMTP( + return MockSMTP( "localhost", 25, 5, @@ -85,7 +85,6 @@ def message(): 0, True, ) - return mailer HTML = """ diff --git a/tests/components/snapcast/test_config_flow.py b/tests/components/snapcast/test_config_flow.py index bb07eae2140..3bdba8b4c58 100644 --- a/tests/components/snapcast/test_config_flow.py +++ b/tests/components/snapcast/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -38,7 +38,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -50,7 +50,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -60,7 +60,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Snapcast" assert result["data"] == {CONF_HOST: "snapserver.test", CONF_PORT: 1705} assert len(mock_create_server.mock_calls) == 1 @@ -80,7 +80,7 @@ async def test_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -91,5 +91,5 @@ async def test_abort( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 9590473f218..89ee211b38f 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -7,7 +7,7 @@ import pytest import voluptuous as vol from homeassistant.bootstrap import async_setup_component -import homeassistant.components.snips as snips +from homeassistant.components import snips from homeassistant.core import HomeAssistant from homeassistant.helpers.intent import ServiceIntentHandler, async_register diff --git a/tests/components/snooz/test_config_flow.py b/tests/components/snooz/test_config_flow.py index 172ca3cd143..4ed4d6184a7 100644 --- a/tests/components/snooz/test_config_flow.py +++ b/tests/components/snooz/test_config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations -from asyncio import Event +from asyncio import Event, sleep from unittest.mock import patch from homeassistant import config_entries @@ -30,7 +30,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" await _test_setup_entry(hass, result["flow_id"]) @@ -44,7 +44,7 @@ async def test_async_step_bluetooth_waits_to_pair(hass: HomeAssistant) -> None: data=SNOOZ_SERVICE_INFO_NOT_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" await _test_pairs(hass, result["flow_id"]) @@ -59,7 +59,7 @@ async def test_async_step_bluetooth_retries_pairing(hass: HomeAssistant) -> None data=SNOOZ_SERVICE_INFO_NOT_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" retry_id = await _test_pairs_timeout(hass, result["flow_id"]) @@ -73,7 +73,7 @@ async def test_async_step_bluetooth_not_snooz(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SNOOZ_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -83,7 +83,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -97,7 +97,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"] # ensure discovered devices are listed as options @@ -119,7 +119,7 @@ async def test_async_step_user_with_found_devices_waits_to_pair( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" await _test_pairs(hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME}) @@ -137,7 +137,7 @@ async def test_async_step_user_with_found_devices_retries_pairing( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} @@ -156,7 +156,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -171,7 +171,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={CONF_NAME: TEST_SNOOZ_DISPLAY_NAME}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -194,7 +194,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -212,7 +212,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -223,7 +223,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -231,7 +231,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -244,7 +244,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -255,7 +255,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await _test_setup_entry( hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} @@ -286,7 +286,7 @@ async def _test_pairs( flow_id, user_input=user_input or {}, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "wait_for_pairing_mode" pairing_mode_entered.set() @@ -298,19 +298,26 @@ async def _test_pairs( async def _test_pairs_timeout( hass: HomeAssistant, flow_id: str, user_input: dict | None = None ) -> str: + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): + """Simulate a timeout waiting for pairing mode.""" + await sleep(0) + raise TimeoutError + with patch( "homeassistant.components.snooz.config_flow.async_process_advertisements", - side_effect=TimeoutError(), + _async_process_advertisements, ): result = await hass.config_entries.flow.async_configure( flow_id, user_input=user_input or {} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "wait_for_pairing_mode" await hass.async_block_till_done() result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing_timeout" return result2["flow_id"] @@ -325,7 +332,7 @@ async def _test_setup_entry( user_input=user_input or {}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, CONF_TOKEN: TEST_PAIRING_TOKEN, diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 81b97c071fd..759a4d6b421 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,15 +1,15 @@ """Tests for the SolarEdge config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch +from aiohttp import ClientError import pytest -from requests.exceptions import ConnectTimeout, HTTPError -from homeassistant import data_entry_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,8 +22,11 @@ API_KEY = "a1b2c3d4e5f6g7h8" def mock_controller(): """Mock a successful Solaredge API.""" api = Mock() - api.get_details.return_value = {"details": {"status": "active"}} - with patch("solaredge.Solaredge", return_value=api): + api.get_details = AsyncMock(return_value={"details": {"status": "active"}}) + with patch( + "homeassistant.components.solaredge.config_flow.aiosolaredge.SolarEdge", + return_value=api, + ): yield api @@ -32,7 +35,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # test with all provided @@ -41,7 +44,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") @@ -63,7 +66,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "already_configured"} @@ -83,7 +86,7 @@ async def test_ignored_entry_does_not_cause_error( context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test" data = result["data"] @@ -103,7 +106,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure @@ -113,25 +116,25 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout - test_api.get_details.side_effect = ConnectTimeout() + test_api.get_details = AsyncMock(side_effect=TimeoutError()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError - test_api.get_details.side_effect = HTTPError() + test_api.get_details = AsyncMock(side_effect=ClientError()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index b1496d18d93..7a6b3af1cde 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -1,6 +1,6 @@ """Tests for the SolarEdge coordinator services.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -25,7 +25,7 @@ def enable_all_entities(entity_registry_enabled_by_default): """Make sure all entities are enabled.""" -@patch("homeassistant.components.solaredge.Solaredge") +@patch("homeassistant.components.solaredge.SolarEdge") async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge, hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: @@ -35,7 +35,9 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( title=DEFAULT_NAME, data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ) - mock_solaredge().get_details.return_value = {"details": {"status": "active"}} + mock_solaredge().get_details = AsyncMock( + return_value={"details": {"status": "active"}} + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -50,7 +52,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( "currentPower": {"power": 0.0}, } } - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -60,7 +62,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lifeTimeData energy is lower than last year, month or day. mock_overview_data["overview"]["lifeTimeData"]["energy"] = 0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -71,7 +73,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # New valid energy values update mock_overview_data["overview"]["lifeTimeData"]["energy"] = 100001 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -82,7 +84,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lastYearData energy is lower than last month or day. mock_overview_data["overview"]["lastYearData"]["energy"] = 0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -100,7 +102,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_overview_data["overview"]["lastYearData"]["energy"] = 0.0 mock_overview_data["overview"]["lastMonthData"]["energy"] = 0.0 mock_overview_data["overview"]["lastDayData"]["energy"] = 0.0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 16f25264b9d..c356a129806 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -4,11 +4,12 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "solarlog_test_1_2_3" assert result2["data"] == {"host": "http://1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -68,12 +69,12 @@ async def test_user(hass: HomeAssistant, test_connect) -> None: flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # tets with all provided result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -84,19 +85,19 @@ async def test_import(hass: HomeAssistant, test_connect) -> None: # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog" assert result["data"][CONF_HOST] == HOST # import with only name result = await flow.async_step_import({CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == DEFAULT_HOST # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -112,19 +113,19 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None result = await flow.async_step_import( {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same HOST and NAME result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "already_configured"} # SHOULD pass, diff HOST (without http://), different NAME result = await flow.async_step_import( {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" assert result["data"][CONF_HOST] == "http://2.2.2.2" @@ -132,6 +133,6 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None result = await flow.async_step_import( {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/solax/test_config_flow.py b/tests/components/solax/test_config_flow.py index c671fe39cec..c787962cc8c 100644 --- a/tests/components/solax/test_config_flow.py +++ b/tests/components/solax/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.solax.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def __mock_real_time_api_success(): @@ -18,7 +19,11 @@ def __mock_real_time_api_success(): def __mock_get_data(): return InverterResponse( - data=None, serial_number="ABCDEFGHIJ", version="2.034.06", type=4 + data=None, + dongle_serial_number="ABCDEFGHIJ", + version="2.034.06", + type=4, + inverter_serial_number="XXXXXXX", ) @@ -27,7 +32,7 @@ async def test_form_success(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} with ( @@ -47,7 +52,7 @@ async def test_form_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert entry_result["type"] == "create_entry" + assert entry_result["type"] is FlowResultType.CREATE_ENTRY assert entry_result["title"] == "ABCDEFGHIJ" assert entry_result["data"] == { CONF_IP_ADDRESS: "192.168.1.87", @@ -62,7 +67,7 @@ async def test_form_connect_error(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} with patch( @@ -74,7 +79,7 @@ async def test_form_connect_error(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80, CONF_PASSWORD: "password"}, ) - assert entry_result["type"] == "form" + assert entry_result["type"] is FlowResultType.FORM assert entry_result["errors"] == {"base": "cannot_connect"} @@ -83,7 +88,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} with patch( @@ -95,5 +100,5 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80, CONF_PASSWORD: "password"}, ) - assert entry_result["type"] == "form" + assert entry_result["type"] is FlowResultType.FORM assert entry_result["errors"] == {"base": "unknown"} diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 04a93bb5a58..8b8548bfe3e 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -5,9 +5,9 @@ from unittest.mock import patch from api.soma_api import SomaApi from requests import RequestException -from homeassistant import data_entry_flow from homeassistant.components.soma import DOMAIN, config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: flow = config_flow.SomaFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_import_abort(hass: HomeAssistant) -> None: @@ -29,7 +29,7 @@ async def test_import_abort(hass: HomeAssistant) -> None: flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -39,7 +39,7 @@ async def test_import_create(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_error_status(hass: HomeAssistant) -> None: @@ -48,7 +48,7 @@ async def test_error_status(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "result_error" @@ -58,7 +58,7 @@ async def test_key_error(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -68,7 +68,7 @@ async def test_exception(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", side_effect=RequestException()): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -79,4 +79,4 @@ async def test_full_flow(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index a01f4d640a1..9084d988ec9 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.somfy_mylink.const import ( CONF_REVERSED_TARGET_IDS, @@ -13,6 +13,7 @@ from homeassistant.components.somfy_mylink.const import ( ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +24,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -46,7 +47,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "MyLink 1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -67,7 +68,7 @@ async def test_form_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -90,7 +91,7 @@ async def test_form_user_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -117,7 +118,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -140,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -163,7 +164,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -182,7 +183,7 @@ async def test_options_not_loaded(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT @pytest.mark.parametrize("reversed", [True, False]) @@ -211,7 +212,7 @@ async def test_options_with_targets(hass: HomeAssistant, reversed) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -219,19 +220,19 @@ async def test_options_with_targets(hass: HomeAssistant, reversed) -> None: user_input={"target_id": "a"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"reverse": reversed}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM result4 = await hass.config_entries.options.async_configure( result3["flow_id"], user_input={"target_id": None}, ) - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_REVERSED_TARGET_IDS: {"a": reversed}, @@ -271,7 +272,7 @@ async def test_form_user_already_configured_from_dhcp(hass: HomeAssistant) -> No await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -292,7 +293,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: hostname="somfy_eeff", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_dhcp_discovery(hass: HomeAssistant) -> None: @@ -307,7 +308,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: hostname="somfy_eeff", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -330,7 +331,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "MyLink 1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 3e48a4b25a8..6bd14e8b581 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect( @@ -45,7 +45,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -65,7 +65,7 @@ async def test_invalid_auth( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -83,7 +83,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -106,14 +106,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() @@ -122,7 +122,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "test-api-key-reauth" @@ -139,7 +139,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -149,7 +149,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -166,7 +166,7 @@ async def test_full_user_flow_advanced_options( DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -179,7 +179,7 @@ async def test_full_user_flow_advanced_options( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -201,7 +201,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -210,6 +210,6 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_UPCOMING_DAYS] == 2 assert result["data"][CONF_WANTED_MAX_ITEMS] == 100 diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index 6ebc2ec5ef4..ab585c5a6d5 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -85,6 +85,24 @@ def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=Non input2.active = True type(mocked_device).get_inputs = AsyncMock(return_value=[input1, input2]) + sound_mode1 = MagicMock() + sound_mode1.title = "Sound Mode 1" + sound_mode1.value = "sound_mode1" + sound_mode1.isAvailable = True + sound_mode2 = MagicMock() + sound_mode2.title = "Sound Mode 2" + sound_mode2.value = "sound_mode2" + sound_mode2.isAvailable = True + sound_mode3 = MagicMock() + sound_mode3.title = "Sound Mode 3" + sound_mode3.value = "sound_mode3" + sound_mode3.isAvailable = False + + soundField = MagicMock() + soundField.currentValue = "sound_mode2" + soundField.candidate = [sound_mode1, sound_mode2, sound_mode3] + type(mocked_device).get_sound_settings = AsyncMock(return_value=[soundField]) + type(mocked_device).set_power = AsyncMock() type(mocked_device).set_sound_settings = AsyncMock() type(mocked_device).listen_notifications = AsyncMock() diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 338207a5d13..8f503360702 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -64,7 +64,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["description_placeholders"] == { CONF_NAME: FRIENDLY_NAME, @@ -77,7 +77,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -91,7 +91,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -100,7 +100,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == { CONF_NAME: MODEL, @@ -119,7 +119,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -135,7 +135,7 @@ async def test_flow_import_without_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT} @@ -163,7 +163,7 @@ async def test_ssdp_bravia(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=ssdp_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_songpal_device" @@ -175,7 +175,7 @@ async def test_sddp_exist(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -188,7 +188,7 @@ async def test_user_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -204,7 +204,7 @@ async def test_import_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -220,7 +220,7 @@ async def test_user_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -237,7 +237,7 @@ async def test_import_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" mocked_device.get_supported_methods.assert_called_once() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 4b1abf8709e..88443bf58b9 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -12,6 +12,7 @@ from songpal import ( SongpalException, VolumeChange, ) +from songpal.notification import SettingChange from homeassistant.components import media_player, songpal from homeassistant.components.media_player import MediaPlayerEntityFeature @@ -47,6 +48,7 @@ SUPPORT_SONGPAL = ( | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.SELECT_SOUND_MODE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF ) @@ -138,6 +140,8 @@ async def test_state(hass: HomeAssistant) -> None: assert attributes["is_volume_muted"] is False assert attributes["source_list"] == ["title1", "title2"] assert attributes["source"] == "title2" + assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"] + assert attributes["sound_mode"] == "Sound Mode 2" assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = dr.async_get(hass) @@ -171,6 +175,8 @@ async def test_state_wireless(hass: HomeAssistant) -> None: assert attributes["is_volume_muted"] is False assert attributes["source_list"] == ["title1", "title2"] assert attributes["source"] == "title2" + assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"] + assert attributes["sound_mode"] == "Sound Mode 2" assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = dr.async_get(hass) @@ -206,6 +212,8 @@ async def test_state_both(hass: HomeAssistant) -> None: assert attributes["is_volume_muted"] is False assert attributes["source_list"] == ["title1", "title2"] assert attributes["source"] == "title2" + assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"] + assert attributes["sound_mode"] == "Sound Mode 2" assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = dr.async_get(hass) @@ -303,6 +311,9 @@ async def test_services(hass: HomeAssistant) -> None: mocked_device2.set_sound_settings.assert_called_once_with("name", "value") mocked_device3.set_sound_settings.assert_called_once_with("name", "value") + await _call(hass, media_player.SERVICE_SELECT_SOUND_MODE, sound_mode="Sound Mode 1") + mocked_device.set_sound_settings.assert_called_with("soundField", "sound_mode1") + async def test_websocket_events(hass: HomeAssistant) -> None: """Test websocket events.""" @@ -315,7 +326,7 @@ async def test_websocket_events(hass: HomeAssistant) -> None: await hass.async_block_till_done() mocked_device.listen_notifications.assert_called_once() - assert mocked_device.on_notification.call_count == 4 + assert mocked_device.on_notification.call_count == 5 notification_callbacks = mocked_device.notification_callbacks @@ -336,6 +347,15 @@ async def test_websocket_events(hass: HomeAssistant) -> None: await notification_callbacks[ContentChange](content_change) assert _get_attributes(hass)["source"] == "title1" + sound_mode_change = MagicMock() + sound_mode_change.target = "soundField" + sound_mode_change.currentValue = "sound_mode1" + await notification_callbacks[SettingChange](sound_mode_change) + assert _get_attributes(hass)["sound_mode"] == "Sound Mode 1" + sound_mode_change.currentValue = "sound_mode2" + await notification_callbacks[SettingChange](sound_mode_change) + assert _get_attributes(hass)["sound_mode"] == "Sound Mode 2" + power_change = MagicMock() power_change.status = False await notification_callbacks[PowerChange](power_change) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 576c9a80799..0eb9b497fbd 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo +from soco.alarms import Alarms from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf @@ -122,6 +123,8 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event): async def _wrapper(): config_entry.add_to_hass(hass) + sonos_alarms = Alarms() + sonos_alarms.last_alarm_list_version = "RINCON_test:0" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() @@ -200,6 +203,7 @@ class SoCoMockFactory: my_speaker_info["zone_name"] = name my_speaker_info["uid"] = mock_soco.uid mock_soco.get_speaker_info = Mock(return_value=my_speaker_info) + mock_soco.add_to_queue = Mock(return_value=10) mock_soco.avTransport = SonosMockService("AVTransport", ip_address) mock_soco.renderingControl = SonosMockService("RenderingControl", ip_address) @@ -300,11 +304,116 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.2"]}}} +class MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__(self, title: str, item_id: str, parent_id: str, item_class: str): + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + +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 == "album_artists" and idstring == "A:ALBUMARTIST/Beatles": + return [ + MockMusicServiceItem( + "All", + idstring + "/", + idstring, + "object.container.playlistContainer.sameArtist", + ), + MockMusicServiceItem( + "A Hard Day's Night", + "A:ALBUMARTIST/Beatles/A%20Hard%20Day's%20Night", + idstring, + "object.container.album.musicAlbum", + ), + MockMusicServiceItem( + "Abbey Road", + "A:ALBUMARTIST/Beatles/Abbey%20Road", + idstring, + "object.container.album.musicAlbum", + ), + ] + # browse_by_id_string works with URL encoded or decoded strings + if search_type == "genres" and idstring in ( + "A:GENRE/Classic%20Rock", + "A:GENRE/Classic Rock", + ): + return [ + MockMusicServiceItem( + "All", + "A:GENRE/Classic%20Rock/", + "A:GENRE/Classic%20Rock", + "object.container.albumlist", + ), + MockMusicServiceItem( + "Bruce Springsteen", + "A:GENRE/Classic%20Rock/Bruce%20Springsteen", + "A:GENRE/Classic%20Rock", + "object.container.person.musicArtist", + ), + MockMusicServiceItem( + "Cream", + "A:GENRE/Classic%20Rock/Cream", + "A:GENRE/Classic%20Rock", + "object.container.person.musicArtist", + ), + ] + if search_type == "composers" and idstring in ( + "A:COMPOSER/Carlos%20Santana", + "A:COMPOSER/Carlos Santana", + ): + return [ + MockMusicServiceItem( + "All", + "A:COMPOSER/Carlos%20Santana/", + "A:COMPOSER/Carlos%20Santana", + "object.container.playlistContainer.sameArtist", + ), + MockMusicServiceItem( + "Between Good And Evil", + "A:COMPOSER/Carlos%20Santana/Between%20Good%20And%20Evil", + "A:COMPOSER/Carlos%20Santana", + "object.container.album.musicAlbum", + ), + MockMusicServiceItem( + "Sacred Fire", + "A:COMPOSER/Carlos%20Santana/Sacred%20Fire", + "A:COMPOSER/Carlos%20Santana", + "object.container.album.musicAlbum", + ), + ] + return [] + + +def mock_get_music_library_information( + search_type: str, search_term: str, full_album_art_uri: bool = True +) -> list[MockMusicServiceItem]: + """Mock the call to get music library information.""" + if search_type == "albums" and search_term == "Abbey Road": + return [ + MockMusicServiceItem( + "Abbey Road", + "A:ALBUM/Abbey%20Road", + "A:ALBUM", + "object.container.album.musicAlbum", + ) + ] + + @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" music_library = MagicMock() music_library.get_sonos_favorites.return_value.update_id = 1 + music_library.browse_by_idstring = mock_browse_by_idstring + music_library.get_music_library_information = mock_get_music_library_information return music_library @@ -494,6 +603,6 @@ def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): 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() + await hass.async_block_till_done(wait_background_tasks=True) return _wrapper diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 186e45e3d84..141013dec20 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -24,9 +25,9 @@ async def test_user_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Initiate a discovery to allow config entry creation @@ -40,7 +41,7 @@ async def test_user_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( patch( @@ -58,7 +59,7 @@ async def test_user_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sonos" assert result2["data"] == {} assert len(mock_setup.mock_calls) == 1 @@ -78,7 +79,7 @@ async def test_user_form_already_created(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -93,7 +94,7 @@ async def test_zeroconf_form( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_payload, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -112,7 +113,7 @@ async def test_zeroconf_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sonos" assert result2["data"] == {} @@ -157,7 +158,7 @@ async def test_ssdp_discovery(hass: HomeAssistant, soco) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Sonos" assert result["data"] == {} @@ -191,7 +192,7 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -210,7 +211,7 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sonos" assert result2["data"] == {} @@ -232,6 +233,6 @@ async def test_zeroconf_form_not_sonos( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_payload, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_sonos_device" assert len(mock_manager.mock_calls) == 0 diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 77bf9a5d12b..f8ac5fc6dbf 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import sonos, zeroconf from homeassistant.components.sonos import SonosDiscoveryManager from homeassistant.components.sonos.const import ( @@ -16,6 +16,7 @@ from homeassistant.components.sonos.const import ( ) from homeassistant.components.sonos.exception import SonosUpdateError from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -46,12 +47,12 @@ async def test_creating_entry_sets_up_media_player( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index cb6303c800d..d8d0e1c3a07 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -38,8 +38,9 @@ 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" + if search_type == "albums" and idstring in ( + "A:ALBUM/Abbey%20Road", + "A:ALBUM/Abbey Road", ): return [ MockMusicServiceItem( diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index c181520b85d..976d3480429 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -7,7 +7,10 @@ import pytest from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, + MediaPlayerEnqueue, ) +from homeassistant.components.media_player.const import ATTR_MEDIA_ENQUEUE +from homeassistant.components.sonos.media_player import LONG_SERVICE_TIMEOUT from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( @@ -16,7 +19,7 @@ from homeassistant.helpers.device_registry import ( DeviceRegistry, ) -from .conftest import SoCoMockFactory +from .conftest import MockMusicServiceItem, SoCoMockFactory async def test_device_registry( @@ -65,35 +68,134 @@ async def test_entity_basic( 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://") +@pytest.mark.parametrize( + ("media_content_type", "media_content_id", "enqueue", "test_result"), + [ + ( + "artist", + "A:ALBUMARTIST/Beatles", + MediaPlayerEnqueue.REPLACE, + { + "title": "All", + "item_id": "A:ALBUMARTIST/Beatles/", + "clear_queue": 1, + "position": None, + "play": 1, + "play_pos": 0, + }, + ), + ( + "genre", + "A:GENRE/Classic%20Rock", + MediaPlayerEnqueue.ADD, + { + "title": "All", + "item_id": "A:GENRE/Classic%20Rock/", + "clear_queue": 0, + "position": None, + "play": 0, + "play_pos": 0, + }, + ), + ( + "album", + "A:ALBUM/Abbey%20Road", + MediaPlayerEnqueue.NEXT, + { + "title": "Abbey Road", + "item_id": "A:ALBUM/Abbey%20Road", + "clear_queue": 0, + "position": 1, + "play": 0, + "play_pos": 0, + }, + ), + ( + "composer", + "A:COMPOSER/Carlos%20Santana", + MediaPlayerEnqueue.PLAY, + { + "title": "All", + "item_id": "A:COMPOSER/Carlos%20Santana/", + "clear_queue": 0, + "position": 1, + "play": 1, + "play_pos": 9, + }, + ), + ( + "artist", + "A:ALBUMARTIST/Beatles/Abbey%20Road", + MediaPlayerEnqueue.REPLACE, + { + "title": "Abbey Road", + "item_id": "A:ALBUMARTIST/Beatles/Abbey%20Road", + "clear_queue": 1, + "position": None, + "play": 1, + "play_pos": 0, + }, + ), + ], +) +async def test_play_media_library( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + media_content_type, + media_content_id, + enqueue, + test_result, +) -> None: + """Test playing local library with a variety of options.""" + sock_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": media_content_type, + "media_content_id": media_content_id, + ATTR_MEDIA_ENQUEUE: enqueue, + }, + blocking=True, + ) + assert sock_mock.clear_queue.call_count == test_result["clear_queue"] + assert sock_mock.add_to_queue.call_count == 1 + assert ( + sock_mock.add_to_queue.call_args_list[0].args[0].title == test_result["title"] + ) + assert ( + sock_mock.add_to_queue.call_args_list[0].args[0].item_id + == test_result["item_id"] + ) + if test_result["position"] is not None: + assert ( + sock_mock.add_to_queue.call_args_list[0].kwargs["position"] + == test_result["position"] + ) + else: + assert "position" not in sock_mock.add_to_queue.call_args_list[0].kwargs + assert ( + sock_mock.add_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert sock_mock.play_from_queue.call_count == test_result["play"] + if test_result["play"] != 0: + assert ( + sock_mock.play_from_queue.call_args_list[0].args[0] + == test_result["play_pos"] + ) _mock_playlists = [ - _MockMusicServiceItem( + MockMusicServiceItem( "playlist1", "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_1", "A:PLAYLISTS", "object.container.playlistContainer", ), - _MockMusicServiceItem( + MockMusicServiceItem( "playlist2", "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_2", "A:PLAYLISTS", diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index 49b87b272d6..2fa951c6a79 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -47,3 +47,4 @@ async def test_subscription_repair_issues( sub_callback(event) await hass.async_block_till_done() assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 1f4ba8d22cd..45068c01bc0 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -242,7 +242,6 @@ async def test_favorites_sensor( # 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"} diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d8499c50bb1..11ce1aa5ddb 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -117,11 +117,12 @@ async def test_switch_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) assert m.called # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON @@ -157,7 +158,7 @@ async def test_alarm_create_delete( alarm_event.variables["alarm_list_version"] = two_alarms["CurrentAlarmListVersion"] sub_callback(event=alarm_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" in entity_registry.entities @@ -169,7 +170,7 @@ async def test_alarm_create_delete( alarm_clock.ListAlarms.return_value = one_alarm sub_callback(event=alarm_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py index bc7de5b7fda..264049ab5fc 100644 --- a/tests/components/soundtouch/test_config_flow.py +++ b/tests/components/soundtouch/test_config_flow.py @@ -26,7 +26,7 @@ async def test_user_flow_create_entry( context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -41,7 +41,7 @@ async def test_user_flow_create_entry( assert len(mock_setup_entry.mock_calls) == 1 - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == DEVICE_1_NAME assert result.get("data") == { CONF_HOST: DEVICE_1_IP, @@ -65,7 +65,7 @@ async def test_user_flow_cannot_connect( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -92,7 +92,7 @@ async def test_zeroconf_flow_create_entry( ), ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "zeroconf_confirm" assert result.get("description_placeholders") == {"name": DEVICE_1_NAME} @@ -105,7 +105,7 @@ async def test_zeroconf_flow_create_entry( assert len(mock_setup_entry.mock_calls) == 1 - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == DEVICE_1_NAME assert result.get("data") == { CONF_HOST: DEVICE_1_IP, diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index f412c71a6ed..f509c91ad20 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -20,13 +20,13 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -41,7 +41,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -51,7 +51,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", @@ -60,7 +60,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting server name to "*Auto Detect" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -70,7 +70,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "*Auto Detect", CONF_SERVER_ID: None, @@ -86,5 +86,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 2b0f803eb6f..446ed527df4 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -46,7 +46,7 @@ async def test_entry_lifecycle(hass: HomeAssistant, mock_api: MagicMock) -> None await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert hass.data[DOMAIN] assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index 1fb61573216..69f97130f8c 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.spider.const import DOMAIN 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 @@ -34,7 +35,7 @@ async def test_user(hass: HomeAssistant, spider) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -50,7 +51,7 @@ async def test_user(hass: HomeAssistant, spider) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -80,7 +81,7 @@ async def test_import(hass: HomeAssistant, spider) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -99,7 +100,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, spider) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Should fail, config exist (flow) @@ -107,5 +108,5 @@ async def test_abort_if_already_setup(hass: HomeAssistant, spider) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 1ab4e46bd55..6de549c8bc7 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest from spotipy import SpotifyException -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.application_credentials import ( ClientCredential, @@ -16,6 +15,7 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -53,14 +53,14 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -72,7 +72,7 @@ async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -96,7 +96,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://accounts.spotify.com/authorize" "?response_type=code&client_id=client" @@ -181,7 +181,7 @@ async def test_abort_if_spotify_error( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -306,7 +306,7 @@ async def test_reauth_account_mismatch( spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" @@ -316,5 +316,5 @@ async def test_abort_if_no_reauth_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth_confirm"} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_account_mismatch" diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index fc122ad1a95..5f91cba1d94 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -262,7 +262,7 @@ YAML_CONFIG_ALL_TEMPLATES = { async def init_integration( hass: HomeAssistant, - config: dict[str, Any] = None, + config: dict[str, Any] | None = None, entry_id: str = "1", source: str = SOURCE_USER, ) -> MockConfigEntry: diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 7b3b0aaf350..93cde0bccdd 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(recorder_mock: Recorder, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -55,7 +55,7 @@ async def test_form(recorder_mock: Recorder, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", @@ -76,7 +76,7 @@ async def test_form_with_value_template( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -89,7 +89,7 @@ async def test_form_with_value_template( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", @@ -107,7 +107,7 @@ async def test_flow_fails_db_url(recorder_mock: Recorder, hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -130,7 +130,7 @@ async def test_flow_fails_invalid_query( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER result5 = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -148,7 +148,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_2, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "query_invalid", } @@ -158,7 +158,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_3, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "query_invalid", } @@ -168,7 +168,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "query": "query_no_read_only", } @@ -178,7 +178,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "query_no_read_only", } @@ -188,7 +188,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_MULTIPLE_QUERIES, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "multiple_queries", } @@ -198,7 +198,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_NO_RESULTS, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -208,7 +208,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG, ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "name": "Get Value", @@ -228,7 +228,7 @@ async def test_flow_fails_invalid_column_name( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "user" result5 = await hass.config_entries.flow.async_configure( @@ -236,7 +236,7 @@ async def test_flow_fails_invalid_column_name( user_input=ENTRY_CONFIG_INVALID_COLUMN_NAME, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "column": "column_invalid", } @@ -246,7 +246,7 @@ async def test_flow_fails_invalid_column_name( user_input=ENTRY_CONFIG, ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "name": "Get Value", @@ -284,7 +284,7 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -300,7 +300,7 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "query": "SELECT 5 as size", @@ -334,7 +334,7 @@ async def test_options_flow_name_previously_removed( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -353,7 +353,7 @@ async def test_options_flow_name_previously_removed( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value Title", "query": "SELECT 5 as size", @@ -436,7 +436,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_OPT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "query": "query_invalid", } @@ -446,7 +446,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_2_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "query_invalid", } @@ -456,7 +456,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_3_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "query_invalid", } @@ -466,7 +466,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY_OPT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "query": "query_no_read_only", } @@ -476,7 +476,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "query_no_read_only", } @@ -486,7 +486,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_MULTIPLE_QUERIES_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "multiple_queries", } @@ -501,7 +501,7 @@ async def test_options_flow_fails_invalid_query( }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "query": "SELECT 5 as size", @@ -540,7 +540,7 @@ async def test_options_flow_fails_invalid_column_name( user_input=ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "column": "column_invalid", } @@ -554,7 +554,7 @@ async def test_options_flow_fails_invalid_column_name( }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "query": "SELECT 5 as value", @@ -589,7 +589,7 @@ async def test_options_flow_db_url_empty( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with ( @@ -611,7 +611,7 @@ async def test_options_flow_db_url_empty( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "query": "SELECT 5 as size", @@ -627,7 +627,7 @@ async def test_full_flow_not_recorder_db( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -650,7 +650,7 @@ async def test_full_flow_not_recorder_db( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", @@ -663,7 +663,7 @@ async def test_full_flow_not_recorder_db( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with ( @@ -686,7 +686,7 @@ async def test_full_flow_not_recorder_db( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://path/to/db.db", @@ -711,7 +711,7 @@ async def test_full_flow_not_recorder_db( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://path/to/db.db", @@ -745,7 +745,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -764,7 +764,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Get Value", "query": "SELECT 5 as value", @@ -775,7 +775,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) } result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -792,7 +792,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert "device_class" not in result3["data"] assert "state_class" not in result3["data"] assert result3["data"] == { diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index cf5721f52f6..409ebca27c0 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -7,11 +7,11 @@ from unittest.mock import patch import pytest import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.util import get_instance from homeassistant.components.sql import validate_sql_select from homeassistant.components.sql.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -21,17 +21,17 @@ from . import YAML_CONFIG_INVALID, YAML_CONFIG_NO_DB, init_integration async def test_setup_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test setup entry.""" config_entry = await init_integration(hass) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_unload_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test unload an entry.""" config_entry = await init_integration(hass) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_setup_config(recorder_mock: Recorder, hass: HomeAssistant) -> None: diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index dc82b658163..0a03bcc291c 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -55,7 +55,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" assert CONF_HOST in result["data_schema"].schema for key in result["data_schema"].schema: @@ -73,7 +73,7 @@ async def test_user_form(hass: HomeAssistant) -> None: CONF_HTTPS: False, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == { CONF_HOST: HOST, @@ -99,14 +99,14 @@ async def test_user_form_timeout(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} # simulate manual input of host result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: HOST2} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "edit" assert CONF_HOST in result2["data_schema"].schema for key in result2["data_schema"].schema: @@ -137,7 +137,7 @@ async def test_user_form_duplicate(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} @@ -162,7 +162,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -186,7 +186,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -201,7 +201,7 @@ async def test_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" @@ -213,7 +213,7 @@ async def test_discovery_no_uuid(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, CONF_HTTPS: False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" @@ -238,7 +238,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" @@ -260,7 +260,7 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -279,4 +279,4 @@ async def test_dhcp_discovery_existing_player(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index c2cc6d5e1a4..12fa7ffd6d6 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -36,8 +36,7 @@ def fixture_hass_tz_info(hass: HomeAssistant, setup_hass_config) -> dt.tzinfo | @pytest.fixture(name="test_date") def fixture_test_date(hass: HomeAssistant, hass_tz_info) -> dt.datetime | None: """Return test datetime for the hass timezone.""" - test_date = dt.datetime(2022, 8, 2, 0, 0, 0, 0, tzinfo=hass_tz_info) - return test_date + return dt.datetime(2022, 8, 2, 0, 0, 0, 0, tzinfo=hass_tz_info) @pytest.fixture(name="mock_config_entry") diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 8d4904bf00d..19e21f0e1a0 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_form( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -44,7 +44,7 @@ async def test_show_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ACCNT_NAME assert "data" in result @@ -74,7 +74,7 @@ async def test_form_invalid_account( flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_account"} @@ -93,7 +93,7 @@ async def test_form_invalid_auth( flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -112,7 +112,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -121,7 +121,7 @@ async def test_flow_entry_already_configured( ) -> None: """Test user input for config_entry that already exists.""" # Verify mock config setup from fixture - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED assert init_integration.data[CONF_ID] == ACCNT_ID assert init_integration.unique_id == ACCNT_ID @@ -135,7 +135,7 @@ async def test_flow_entry_already_configured( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input_second ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -144,7 +144,7 @@ async def test_flow_multiple_configs( ) -> None: """Test multiple config entries.""" # Verify mock config setup from fixture - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED assert init_integration.data[CONF_ID] == ACCNT_ID assert init_integration.unique_id == ACCNT_ID @@ -156,7 +156,7 @@ async def test_flow_multiple_configs( ) # Verify created - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ACCNT_NAME_2 assert "data" in result diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py index e2411fd4688..2a02e44b9b0 100644 --- a/tests/components/srp_energy/test_init.py +++ b/tests/components/srp_energy/test_init.py @@ -6,12 +6,12 @@ from homeassistant.core import HomeAssistant async def test_setup_entry(hass: HomeAssistant, init_integration) -> None: """Test setup entry.""" - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED async def test_unload_entry(hass: HomeAssistant, init_integration) -> None: """Test being able to unload an entry.""" - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index a46caf904b7..7369d07f77a 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry async def test_loading_sensors(hass: HomeAssistant, init_integration) -> None: """Test the srp energy sensors.""" # Validate the Config Entry was initialized - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED # Check sensors were loaded assert len(hass.states.async_all()) == 1 diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index 0d7d7cc0731..291bf143cbf 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -5,6 +5,7 @@ import requests_mock from homeassistant import config_entries from homeassistant.components.starline import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_APP_ID = "666" TEST_APP_SECRET = "appsecret" @@ -45,7 +46,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" result = await hass.config_entries.flow.async_configure( @@ -55,7 +56,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: config_flow.CONF_APP_SECRET: TEST_APP_SECRET, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_user" result = await hass.config_entries.flow.async_configure( @@ -65,7 +66,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Application {TEST_APP_ID}" @@ -83,7 +84,7 @@ async def test_step_auth_app_code_falls(hass: HomeAssistant) -> None: config_flow.CONF_APP_SECRET: TEST_APP_SECRET, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" assert result["errors"] == {"base": "error_auth_app"} @@ -106,7 +107,7 @@ async def test_step_auth_app_token_falls(hass: HomeAssistant) -> None: config_flow.CONF_APP_SECRET: TEST_APP_SECRET, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" assert result["errors"] == {"base": "error_auth_app"} @@ -123,6 +124,6 @@ async def test_step_auth_user_falls(hass: HomeAssistant) -> None: config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, } ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_user" assert result["errors"] == {"base": "error_auth_user"} diff --git a/tests/components/starlink/test_config_flow.py b/tests/components/starlink/test_config_flow.py index 5b0e122ad5d..613e9b0fc7a 100644 --- a/tests/components/starlink/test_config_flow.py +++ b/tests/components/starlink/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Starlink config flow.""" -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.starlink.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .patchers import DEVICE_FOUND_PATCHER, NO_DEVICE_PATCHER, SETUP_ENTRY_PATCHER @@ -27,7 +28,7 @@ async def test_flow_user_fails_can_succeed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: @@ -37,7 +38,7 @@ async def test_flow_user_fails_can_succeed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == user_input @@ -57,7 +58,7 @@ async def test_flow_user_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == user_input @@ -85,5 +86,5 @@ async def test_flow_user_duplicate_abort(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 03e9787b6c0..62a1ee41236 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -31,7 +31,7 @@ async def test_successful_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.entry_id in hass.data[DOMAIN] diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index e24909e3f53..f9222e4bacf 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest import voluptuous as vol -import homeassistant.components.statsd as statsd +from homeassistant.components import statsd from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 00b47ea48bd..9292f58d231 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch import steam -from homeassistant import data_entry_flow from homeassistant.components.steam_online.const import CONF_ACCOUNTS, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from . import ( @@ -42,7 +42,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_OPTIONS @@ -56,7 +56,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -68,7 +68,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -79,7 +79,7 @@ async def test_flow_user_invalid_account(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_account" @@ -91,7 +91,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -104,7 +104,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -121,20 +121,20 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" new_conf = CONF_DATA | {CONF_API_KEY: "1234567890"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf @@ -153,7 +153,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -162,7 +162,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 @@ -187,7 +187,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: return_value=True, ), ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -196,7 +196,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} assert len(er.async_get(hass).entities) == 0 @@ -208,7 +208,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: servicemock.side_effect = steam.api.HTTPTimeoutError result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -217,7 +217,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS @@ -227,7 +227,7 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: with patch_interface_private(): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -236,5 +236,5 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS diff --git a/tests/components/steam_online/test_init.py b/tests/components/steam_online/test_init.py index 48584dab7a5..ccc7690aae3 100644 --- a/tests/components/steam_online/test_init.py +++ b/tests/components/steam_online/test_init.py @@ -17,7 +17,7 @@ async def test_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -33,7 +33,7 @@ async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None: interface.side_effect = steam.api.HTTPError("401") await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/steamist/__init__.py b/tests/components/steamist/__init__.py index 47fa2236849..77e3efca1f0 100644 --- a/tests/components/steamist/__init__.py +++ b/tests/components/steamist/__init__.py @@ -74,7 +74,7 @@ async def _async_setup_entry_with_status( with _patch_status(status, client): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED return client, config_entry diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index 9480703af9f..40578113bb3 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1" assert result2["data"] == { "host": "127.0.0.1", @@ -76,7 +76,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -97,7 +97,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEVICE_NAME assert result2["data"] == DEFAULT_ENTRY_DATA assert result2["context"]["unique_id"] == FORMATTED_MAC_ADDRESS @@ -121,7 +121,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -142,7 +142,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -153,13 +153,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -167,13 +167,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -189,7 +189,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEVICE_NAME assert result3["data"] == DEFAULT_ENTRY_DATA mock_setup.assert_called_once() @@ -199,7 +199,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -207,7 +207,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -221,7 +221,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DISCOVERY_30303, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -231,7 +231,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -245,7 +245,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -260,7 +260,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -274,7 +274,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -291,7 +291,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -305,7 +305,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -325,7 +325,7 @@ async def test_discovered_by_dhcp_discovery_fails(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -345,7 +345,7 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_steamist_device" @@ -374,7 +374,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == FORMATTED_MAC_ADDRESS @@ -409,7 +409,7 @@ async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reloa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert not mock_setup.called assert not mock_setup_entry.called diff --git a/tests/components/steamist/test_init.py b/tests/components/steamist/test_init.py index 32400449d0d..96ea59afda2 100644 --- a/tests/components/steamist/test_init.py +++ b/tests/components/steamist/test_init.py @@ -49,7 +49,7 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: ) await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry_later(hass: HomeAssistant) -> None: @@ -65,7 +65,7 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_fills_unique_id_with_directed_discovery( @@ -101,7 +101,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == FORMATTED_MAC_ADDRESS assert config_entry.data[CONF_NAME] == DEVICE_NAME diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 9830022203a..3664527cbcf 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -29,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Overijssel" assert result2.get("data") == { CONF_PROVINCE: "Overijssel", @@ -55,5 +55,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/stookwijzer/test_config_flow.py b/tests/components/stookwijzer/test_config_flow.py index 590c93bb3c1..732e8abfc98 100644 --- a/tests/components/stookwijzer/test_config_flow.py +++ b/tests/components/stookwijzer/test_config_flow.py @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert "flow_id" in result @@ -32,7 +32,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { "location": { "latitude": 1.0, diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index ab42141c667..e642b209146 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -59,8 +59,7 @@ def frame_image_data(frame_i, total_frames): img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) - return img + return np.clip(img, 0, 255) def generate_video(encoder, container_format, duration): diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 9ce23d99152..280d15cd1ef 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -14,7 +14,6 @@ from __future__ import annotations import asyncio from collections.abc import Generator -from http import HTTPStatus import logging import threading from unittest.mock import Mock, patch @@ -87,6 +86,17 @@ class HLSSync: self._num_recvs = 0 self._num_finished = 0 + def on_resp(): + self._num_finished += 1 + self.check_requests_ready() + + class SyncResponse(web.Response): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + on_resp() + + self.response = SyncResponse + def reset_request_pool(self, num_requests: int, reset_finished=True): """Use to reset the request counter between segments.""" self._num_recvs = 0 @@ -120,12 +130,6 @@ class HLSSync: self.check_requests_ready() return self._original_not_found() - def response(self, body, headers=None, status=HTTPStatus.OK): - """Intercept the Response call so we know when the web handler is finished.""" - self._num_finished += 1 - self.check_requests_ready() - return self._original_response(body=body, headers=headers, status=status) - async def recv(self, output: StreamOutput, **kw): """Intercept the recv call so we know when the response is blocking on recv.""" self._num_recvs += 1 @@ -164,7 +168,7 @@ def hls_sync(): ), patch( "homeassistant.components.stream.hls.web.Response", - side_effect=sync.response, + new=sync.response, ), ): yield sync diff --git a/tests/components/streamlabswater/test_config_flow.py b/tests/components/streamlabswater/test_config_flow.py index 4efe80b31e5..0cee3b8b088 100644 --- a/tests/components/streamlabswater/test_config_flow.py +++ b/tests/components/streamlabswater/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Streamlabs" assert result["data"] == {CONF_API_KEY: "abc"} assert len(mock_setup_entry.mock_calls) == 1 @@ -49,7 +49,7 @@ async def test_form_cannot_connect( {CONF_API_KEY: "abc"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): @@ -59,7 +59,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Streamlabs" assert result["data"] == {CONF_API_KEY: "abc"} assert len(mock_setup_entry.mock_calls) == 1 @@ -80,7 +80,7 @@ async def test_form_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> {CONF_API_KEY: "abc"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): @@ -90,7 +90,7 @@ async def test_form_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Streamlabs" assert result["data"] == {CONF_API_KEY: "abc"} assert len(mock_setup_entry.mock_calls) == 1 @@ -118,7 +118,7 @@ async def test_form_entry_already_exists(hass: HomeAssistant) -> None: {CONF_API_KEY: "abc"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -132,7 +132,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Streamlabs" assert result["data"] == {CONF_API_KEY: "abc"} assert len(mock_setup_entry.mock_calls) == 1 @@ -153,7 +153,7 @@ async def test_import_cannot_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -170,7 +170,7 @@ async def test_import_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -190,5 +190,5 @@ async def test_import_entry_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index a06c635bcfd..165a520c653 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -370,9 +370,9 @@ async def test_config_entry_unload( ) -> None: """Test we can unload config entry.""" config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_restore_state( @@ -388,7 +388,7 @@ async def test_restore_state( config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert state.state == timestamp diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 76bad81bff4..9bddeeee051 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.subaru import config_flow from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from .conftest import ( @@ -45,7 +46,7 @@ async def test_user_form_init(user_form) -> None: assert user_form["errors"] is None assert user_form["handler"] == DOMAIN assert user_form["step_id"] == "user" - assert user_form["type"] == "form" + assert user_form["type"] is FlowResultType.FORM async def test_user_form_repeat_identifier(hass: HomeAssistant, user_form) -> None: @@ -64,7 +65,7 @@ async def test_user_form_repeat_identifier(hass: HomeAssistant, user_form) -> No TEST_CREDS, ) assert len(mock_connect.mock_calls) == 0 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -79,7 +80,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant, user_form) -> None: TEST_CREDS, ) assert len(mock_connect.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -94,7 +95,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant, user_form) -> None: TEST_CREDS, ) assert len(mock_connect.mock_calls) == 1 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -207,7 +208,7 @@ async def test_two_factor_request_fail( user_input={config_flow.CONF_CONTACT_METHOD: "email@addr.com"}, ) assert len(mock_two_factor_request.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "two_factor_request_failed" @@ -302,7 +303,7 @@ async def test_pin_form_bad_pin_format(hass: HomeAssistant, pin_form) -> None: ) assert len(mock_test_pin.mock_calls) == 0 assert len(mock_update_saved_pin.mock_calls) == 1 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "bad_pin_format"} @@ -361,7 +362,7 @@ async def test_pin_form_incorrect_pin(hass: HomeAssistant, pin_form) -> None: ) assert len(mock_test_pin.mock_calls) == 1 assert len(mock_update_saved_pin.mock_calls) == 1 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "incorrect_pin"} @@ -370,7 +371,7 @@ async def test_option_flow_form(options_form) -> None: assert options_form["description_placeholders"] is None assert options_form["errors"] is None assert options_form["step_id"] == "init" - assert options_form["type"] == "form" + assert options_form["type"] is FlowResultType.FORM async def test_option_flow(hass: HomeAssistant, options_form) -> None: @@ -381,7 +382,7 @@ async def test_option_flow(hass: HomeAssistant, options_form) -> None: CONF_UPDATE_ENABLED: False, }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UPDATE_ENABLED: False, } diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index a4ab52151d1..1d689ffe0d6 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.suez_water.config_flow.SuezClient"): @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA @@ -64,7 +64,7 @@ async def test_form_invalid_auth( MOCK_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with patch("homeassistant.components.suez_water.config_flow.SuezClient"): @@ -74,7 +74,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA @@ -100,7 +100,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: MOCK_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -124,7 +124,7 @@ async def test_form_error( MOCK_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with patch( @@ -135,7 +135,7 @@ async def test_form_error( MOCK_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -149,7 +149,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA @@ -175,7 +175,7 @@ async def test_import_error( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -196,7 +196,7 @@ async def test_importing_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -214,5 +214,5 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index ef13595ed59..c5b5b29976c 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} @@ -51,7 +51,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -65,7 +65,7 @@ async def test_import_flow( data={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index f7bdb5eb17b..e315ea8cdcd 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -5,8 +5,7 @@ from datetime import datetime from freezegun import freeze_time import pytest -from homeassistant.components import sun -import homeassistant.components.automation as automation +from homeassistant.components import automation, sun from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -128,8 +127,11 @@ async def test_sunset_trigger_with_offset(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event", "offset")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event }}" + " - {{ trigger.offset }}" + ) }, }, } diff --git a/tests/components/sunweg/common.py b/tests/components/sunweg/common.py index 616f5c0137f..096113f6609 100644 --- a/tests/components/sunweg/common.py +++ b/tests/components/sunweg/common.py @@ -12,6 +12,7 @@ SUNWEG_USER_INPUT = { SUNWEG_MOCK_ENTRY = MockConfigEntry( domain=DOMAIN, + unique_id=0, data={ CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 84957a419dd..80b6a946749 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -2,14 +2,15 @@ from unittest.mock import patch -from sunweg.api import APIHelper +from sunweg.api import APIHelper, SunWegApiError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType -from .common import SUNWEG_USER_INPUT +from .common import SUNWEG_MOCK_ENTRY, SUNWEG_USER_INPUT from tests.common import MockConfigEntry @@ -20,7 +21,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -35,17 +36,111 @@ async def test_incorrect_login(hass: HomeAssistant) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} -async def test_no_plants_on_account(hass: HomeAssistant) -> None: - """Test registering an integration with no plants available.""" +async def test_server_unavailable(hass: HomeAssistant) -> None: + """Test when the SunWEG server don't respond.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + with patch.object( + APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], SUNWEG_USER_INPUT + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "timeout_connect"} + + +async def test_reauth(hass: HomeAssistant, plant_fixture, inverter_fixture) -> None: + """Test reauth flow.""" + mock_entry = SUNWEG_MOCK_ENTRY + mock_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries() + assert len(entries) == 1 + assert entries[0].data[CONF_USERNAME] == SUNWEG_MOCK_ENTRY.data[CONF_USERNAME] + assert entries[0].data[CONF_PASSWORD] == SUNWEG_MOCK_ENTRY.data[CONF_PASSWORD] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch.object(APIHelper, "authenticate", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + with patch.object( + APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "timeout_connect"} + + 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"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + entries = hass.config_entries.async_entries() + + assert len(entries) == 1 + assert entries[0].data[CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] + assert entries[0].data[CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] + + +async def test_no_plants_on_account(hass: HomeAssistant) -> None: + """Test registering an integration with wrong auth then with no plants available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch.object(APIHelper, "authenticate", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], SUNWEG_USER_INPUT + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + with ( patch.object(APIHelper, "authenticate", return_value=True), patch.object(APIHelper, "listPlants", return_value=[]), @@ -54,7 +149,7 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_plants" @@ -63,26 +158,25 @@ async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - 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), + patch.object( + APIHelper, "listPlants", return_value=[plant_fixture, plant_fixture] + ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plant" - user_input = {CONF_PLANT_ID: 123456} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], {CONF_PLANT_ID: 123456} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == 123456 @@ -93,7 +187,6 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() with ( patch.object(APIHelper, "authenticate", return_value=True), @@ -104,10 +197,10 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == 123456 @@ -120,7 +213,6 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() with ( patch.object(APIHelper, "authenticate", return_value=True), @@ -131,8 +223,8 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sunweg/test_init.py b/tests/components/sunweg/test_init.py index cc2e880d82e..41edda38a5a 100644 --- a/tests/components/sunweg/test_init.py +++ b/tests/components/sunweg/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.sunweg.const import DOMAIN, DeviceType from homeassistant.components.sunweg.sensor_types.sensor_entity_description import ( SunWEGSensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -193,3 +194,16 @@ async def test_sunwegdata_get_data_never_reset() -> None: never_resets=entity_description.never_resets, previous_value_drop_threshold=entity_description.previous_value_drop_threshold, ) == (2.8, None) + + +async def test_reauth_started(hass: HomeAssistant) -> None: + """Test reauth flow started.""" + mock_entry = SUNWEG_MOCK_ENTRY + mock_entry.add_to_hass(hass) + with patch.object(APIHelper, "authenticate", return_value=False): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_entry.state is ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index 67ee5d81247..c3c13195aca 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sure Petcare" assert result2["data"] == { "username": "test-username", @@ -67,7 +67,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -89,7 +89,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -111,7 +111,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -142,7 +142,7 @@ async def test_flow_entry_already_exists( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -165,7 +165,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -179,7 +179,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -202,7 +202,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -217,7 +217,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "invalid_auth" @@ -240,7 +240,7 @@ async def test_reauthentication_cannot_connect(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -255,7 +255,7 @@ async def test_reauthentication_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "cannot_connect" @@ -278,7 +278,7 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -293,5 +293,5 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "unknown" diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index 9400423ff98..47969cdc9dd 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -34,7 +34,7 @@ async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == "swiss_public_transport" assert result["data_schema"] == config_flow.DATA_SCHEMA @@ -52,7 +52,7 @@ async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: user_input=MOCK_DATA_STEP, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "test_start test_destination" assert result["data"] == MOCK_DATA_STEP @@ -83,7 +83,7 @@ async def test_flow_user_init_data_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == text_error # Recover @@ -94,7 +94,7 @@ async def test_flow_user_init_data_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "test_start test_destination" assert result["data"] == MOCK_DATA_STEP @@ -124,7 +124,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_import( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == MOCK_DATA_IMPORT assert len(mock_setup_entry.mock_calls) == 1 @@ -179,7 +179,7 @@ async def test_import_error(hass: HomeAssistant, raise_error, text_error) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == text_error @@ -199,5 +199,5 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: data=MOCK_DATA_IMPORT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 60c79fdf6a8..e9764d59d7c 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -4,12 +4,16 @@ 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 typing import Any + +from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.loader import bind_hass @@ -36,3 +40,31 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + +class MockSwitch(SwitchEntity): + """Mocked switch entity.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__(self, name: str | None, state: str) -> None: + """Initialize the mock switch entity.""" + self._attr_name = name + self._attr_is_on = state == STATE_ON + + def turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self._attr_is_on = True + + def turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self._attr_is_on = False + + +def get_mock_switch_entities() -> list[MockSwitch]: + """Return a list of mock switch entities.""" + return [ + MockSwitch("AC", STATE_ON), + MockSwitch("AC", STATE_OFF), + MockSwitch(None, STATE_OFF), + ] diff --git a/tests/components/switch/conftest.py b/tests/components/switch/conftest.py deleted file mode 100644 index c526ef4c4fe..00000000000 --- a/tests/components/switch/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -"""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 c35f7261afc..9ad656bcc2b 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index d69d8a547aa..cd0a67fa992 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory @@ -217,8 +217,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -236,8 +238,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -300,8 +304,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -360,9 +366,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 874210a32bc..c528f982ebb 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory @@ -212,15 +212,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -236,15 +233,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -260,15 +254,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -332,15 +323,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -397,15 +385,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 62801346744..aa3e4ccce58 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -9,20 +9,21 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import common +from .common import MockSwitch from tests.common import ( MockUser, help_test_all, import_and_test_deprecated_constant_enum, + setup_test_component_platform, ) @pytest.fixture(autouse=True) -def entities(hass): +def entities(hass: HomeAssistant, mock_switch_entities: list[MockSwitch]): """Initialize the test switch.""" - platform = getattr(hass.components, "test.switch") - platform.init() - return platform.ENTITIES + setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) + return mock_switch_entities async def test_methods( diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 59b7d7fadcd..206ae232d56 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -46,7 +46,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -91,7 +91,7 @@ async def test_config_flow_registered_entity( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -104,7 +104,7 @@ async def test_config_flow_registered_entity( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -158,7 +158,7 @@ async def test_options( assert config_entry result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema schema_key = next(k for k in schema if k == CONF_INVERT) @@ -170,7 +170,7 @@ async def test_options( CONF_INVERT: False, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ENTITY_ID: "switch.ceiling", CONF_INVERT: False, diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index c74b14cc91c..266d0fd0409 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -949,7 +949,7 @@ async def test_migrate( await hass.async_block_till_done() # Check migration was successful and added invert option - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.options == { CONF_ENTITY_ID: "switch.test", CONF_INVERT: False, @@ -988,7 +988,7 @@ async def test_migrate_from_future( await hass.async_block_till_done() # Check migration was not successful and did not add invert option - assert config_entry.state == ConfigEntryState.MIGRATION_ERROR + assert config_entry.state is ConfigEntryState.MIGRATION_ERROR assert config_entry.options == { CONF_ENTITY_ID: "switch.test", CONF_TARGET_DOMAIN: target_domain, diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 99c44365353..c9132972ab4 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -29,7 +29,7 @@ async def test_form(hass: HomeAssistant, test_cucode_in_coordinator_data) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant, test_cucode_in_coordinator_data) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -84,7 +84,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -131,7 +131,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -177,5 +177,5 @@ async def test_form_entry_exists(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == FlowResultType.ABORT + assert form_result["type"] is FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 3d53dd2848e..a62a100f55a 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -46,7 +46,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch_async_setup_entry() as mock_setup_entry: @@ -56,7 +56,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", @@ -73,7 +73,7 @@ async def test_bluetooth_discovery_requires_password(hass: HomeAssistant) -> Non context={"source": SOURCE_BLUETOOTH}, data=WOHAND_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "password" with patch_async_setup_entry() as mock_setup_entry: @@ -83,7 +83,7 @@ async def test_bluetooth_discovery_requires_password(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Bot 923B" assert result["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", @@ -101,14 +101,14 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WOLOCK_SERVICE_INFO, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} @@ -125,7 +125,7 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} @@ -145,7 +145,7 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -175,7 +175,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -186,7 +186,7 @@ async def test_async_step_bluetooth_not_switchbot(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=NOT_SWITCHBOT_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -197,7 +197,7 @@ async def test_async_step_bluetooth_not_connectable(hass: HomeAssistant) -> None context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO_NOT_CONNECTABLE, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -211,7 +211,7 @@ async def test_user_setup_wohand(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -222,7 +222,7 @@ async def test_user_setup_wohand(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", @@ -252,7 +252,7 @@ async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -266,7 +266,7 @@ async def test_user_setup_wocurtain(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -277,7 +277,7 @@ async def test_user_setup_wocurtain(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -302,7 +302,7 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -313,7 +313,7 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -337,7 +337,7 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -345,7 +345,7 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> result["flow_id"], {CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "password" assert result2["errors"] is None @@ -356,7 +356,7 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Bot 923B" assert result3["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", @@ -377,7 +377,7 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "password" assert result["errors"] is None @@ -388,7 +388,7 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Bot 923B" assert result2["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", @@ -409,14 +409,14 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} @@ -433,7 +433,7 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} @@ -453,7 +453,7 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -475,14 +475,14 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {} @@ -498,7 +498,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {"base": "auth_failed"} assert "error from api" in result["description_placeholders"]["error_detail"] @@ -526,7 +526,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -548,14 +548,14 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {} @@ -571,7 +571,7 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -588,7 +588,7 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -597,14 +597,14 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: USER_INPUT, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} @@ -624,7 +624,7 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -645,7 +645,7 @@ async def test_user_setup_wosensor(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -656,7 +656,7 @@ async def test_user_setup_wosensor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Meter EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -675,7 +675,7 @@ async def test_user_no_devices(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -688,7 +688,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": SOURCE_BLUETOOTH}, data=WOCURTAIN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch( @@ -699,7 +699,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -707,7 +707,7 @@ async def test_async_step_user_takes_precedence_over_discovery( user_input={}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Curtain EEFF" assert result2["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -740,7 +740,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -752,7 +752,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 3 assert len(mock_setup_entry.mock_calls) == 2 @@ -763,7 +763,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -775,7 +775,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 6 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/switchbot_cloud/test_config_flow.py b/tests/components/switchbot_cloud/test_config_flow.py index 47758d50582..1d49b503ef2 100644 --- a/tests/components/switchbot_cloud/test_config_flow.py +++ b/tests/components/switchbot_cloud/test_config_flow.py @@ -32,7 +32,7 @@ async def _fill_out_form_and_assert_entry_created( ) await hass.async_block_till_done() - assert result_configure["type"] == FlowResultType.CREATE_ENTRY + assert result_configure["type"] is FlowResultType.CREATE_ENTRY assert result_configure["title"] == ENTRY_TITLE assert result_configure["data"] == { CONF_API_TOKEN: "test-token", @@ -46,7 +46,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result_init["type"] == FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert not result_init["errors"] await _fill_out_form_and_assert_entry_created( @@ -82,7 +82,7 @@ async def test_form_fails( }, ) - assert result_configure["type"] == FlowResultType.FORM + assert result_configure["type"] is FlowResultType.FORM assert result_configure["errors"] == {"base": message} await hass.async_block_till_done() diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index e9f0a0a475d..25ea370efe5 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -55,7 +55,7 @@ async def test_setup_entry_success( entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -104,7 +104,7 @@ async def test_setup_entry_fails_when_refreshing( mock_get_status.side_effect = CannotConnect entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py index e03c8eb645f..913424abae5 100644 --- a/tests/components/switcher_kis/test_config_flow.py +++ b/tests/components/switcher_kis/test_config_flow.py @@ -23,7 +23,7 @@ async def test_import(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Switcher" assert result["data"] == {} @@ -51,7 +51,7 @@ async def test_user_setup(hass: HomeAssistant, mock_bridge) -> None: assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -60,7 +60,7 @@ async def test_user_setup(hass: HomeAssistant, mock_bridge) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Switcher" assert result2["result"].data == {} @@ -78,13 +78,13 @@ async def test_user_setup_abort_no_devices_found( assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -104,5 +104,5 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: DOMAIN, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index d97226e422c..82cbd85ffaa 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from aiosyncthing.exceptions import UnauthorizedError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.syncthing.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_show_setup_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -54,7 +55,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: CONF_VERIFY_SSL: VERIFY_SSL, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:8384" assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_URL] == URL @@ -76,7 +77,7 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +91,7 @@ async def test_flow_invalid_auth(hass: HomeAssistant) -> None: data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["token"] == "invalid_auth" @@ -104,5 +105,5 @@ async def test_flow_cannot_connect(hass: HomeAssistant) -> None: data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 90470431ade..b79e63e1ce7 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import patch from pysyncthru import SyncThruAPINotSupported -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.syncthru.config_flow import SyncThru from homeassistant.components.syncthru.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -45,7 +46,7 @@ async def test_show_setup_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -69,7 +70,7 @@ async def test_already_configured_by_url( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_NAME] == FIXTURE_USER_INPUT[CONF_NAME] assert result["result"].unique_id == udn @@ -84,7 +85,7 @@ async def test_syncthru_not_supported(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "syncthru_not_supported"} @@ -101,7 +102,7 @@ async def test_unknown_state(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown_state"} @@ -123,7 +124,7 @@ async def test_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert len(mock_setup_entry.mock_calls) == 1 @@ -151,7 +152,7 @@ async def test_ssdp(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert CONF_URL in result["data_schema"].schema for k in result["data_schema"].schema: diff --git a/tests/components/synology_dsm/snapshots/test_config_flow.ambr b/tests/components/synology_dsm/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..807ec764e52 --- /dev/null +++ b/tests/components/synology_dsm/snapshots/test_config_flow.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_discovered_via_zeroconf + dict({ + 'host': '192.168.1.5', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5001, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_form_ssdp + dict({ + 'host': '192.168.1.5', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5001, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 1234, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user.1 + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5000, + 'ssl': False, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user_2sa + dict({ + 'device_token': 'Dév!cè_T0k€ñ', + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5001, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user_vdsm + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 1234, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 67da3712983..85814f84aad 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -11,20 +11,15 @@ from synology_dsm.exceptions import ( SynologyDSMLoginInvalidException, SynologyDSMRequestException, ) +from syrupy import SnapshotAssertion -from homeassistant import data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( CONF_SNAPSHOT_QUALITY, - CONF_VOLUMES, - DEFAULT_PORT, - DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, DEFAULT_SNAPSHOT_QUALITY, DEFAULT_TIMEOUT, - DEFAULT_USE_SSL, - DEFAULT_VERIFY_SSL, DOMAIN, ) from homeassistant.config_entries import ( @@ -34,7 +29,6 @@ from homeassistant.config_entries import ( SOURCE_ZEROCONF, ) from homeassistant.const import ( - CONF_DISKS, CONF_HOST, CONF_MAC, CONF_PASSWORD, @@ -46,6 +40,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .consts import ( DEVICE_TOKEN, @@ -149,12 +144,16 @@ def mock_controller_service_failed(): @pytest.mark.usefixtures("mock_setup_entry") -async def test_user(hass: HomeAssistant, service: MagicMock) -> None: +async def test_user( + hass: HomeAssistant, + service: MagicMock, + snapshot: SnapshotAssertion, +) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -174,19 +173,10 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - assert result["data"][CONF_SSL] == USE_SSL - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot service.information.serial = SERIAL_2 with patch( @@ -205,23 +195,16 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == DEFAULT_PORT - assert not result["data"][CONF_SSL] - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") -async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: +async def test_user_2sa( + hass: HomeAssistant, service_2sa: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test user with 2sa authentication config.""" with patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSM", @@ -232,7 +215,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "2sa" # Failed the first time because was too slow to enter the code @@ -242,7 +225,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP_CODE: "000000"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "2sa" assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} @@ -258,23 +241,16 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: result["flow_id"], {CONF_OTP_CODE: "123456"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == DEFAULT_PORT_SSL - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") == DEVICE_TOKEN - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") -async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: +async def test_user_vdsm( + hass: HomeAssistant, service_vdsm: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test user config.""" with patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSM", @@ -283,7 +259,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -303,19 +279,10 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - assert result["data"][CONF_SSL] == USE_SSL - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") @@ -350,7 +317,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -364,7 +331,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -396,7 +363,7 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -412,7 +379,7 @@ async def test_login_failed(hass: HomeAssistant, service: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_USERNAME: "invalid_auth"} @@ -429,7 +396,7 @@ async def test_connection_failed(hass: HomeAssistant, service: MagicMock) -> Non data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -444,7 +411,7 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock) -> None: data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -462,12 +429,14 @@ async def test_missing_data_after_login( context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @pytest.mark.usefixtures("mock_setup_entry") -async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: +async def test_form_ssdp( + hass: HomeAssistant, service: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test we can setup from ssdp.""" result = await hass.config_entries.flow.async_init( @@ -483,7 +452,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -495,19 +464,10 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "mydsm" - assert result["data"][CONF_HOST] == "192.168.1.5" - assert result["data"][CONF_PORT] == 5001 - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") @@ -539,7 +499,7 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -582,7 +542,7 @@ async def test_skip_reconfig_ssdp( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -615,7 +575,7 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -637,7 +597,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # Scan interval @@ -646,7 +606,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT assert config_entry.options[CONF_SNAPSHOT_QUALITY] == DEFAULT_SNAPSHOT_QUALITY @@ -657,14 +617,16 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30, CONF_SNAPSHOT_QUALITY: 0}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 @pytest.mark.usefixtures("mock_setup_entry") -async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) -> None: +async def test_discovered_via_zeroconf( + hass: HomeAssistant, service: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test we can setup from zeroconf.""" result = await hass.config_entries.flow.async_init( @@ -682,7 +644,7 @@ async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -694,19 +656,10 @@ async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "mydsm" - assert result["data"][CONF_HOST] == "192.168.1.5" - assert result["data"][CONF_PORT] == 5001 - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") @@ -728,5 +681,5 @@ async def test_discovered_via_zeroconf_missing_mac( properties={}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_mac_address" diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 25c4d69dfee..13d568e6137 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch from synology_dsm.exceptions import SynologyDSMLoginInvalidException -from homeassistant import data_entry_flow from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES from homeassistant.const import ( CONF_HOST, @@ -15,6 +14,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME @@ -57,7 +57,7 @@ async def test_reauth_triggered(hass: HomeAssistant) -> None: patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", return_value={ - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "flow_id": "mock_flow", "step_id": "reauth_confirm", }, diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index e806014dcd6..2a792d174f8 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -49,7 +49,9 @@ def dsm_with_photos() -> MagicMock: dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)]) dsm.photos.get_items_from_album = AsyncMock( - return_value=[SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm")] + return_value=[ + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", False) + ] ) dsm.photos.get_item_thumbnail_url = AsyncMock( return_value="http://my.thumbnail.url" @@ -428,6 +430,8 @@ async def test_media_view( # success dsm_with_photos.photos.download_item = AsyncMock(return_value=b"xxxx") - tempfile.tempdir = tmp_path - result = await view.get(request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg") - assert isinstance(result, web.Response) + with patch.object(tempfile, "tempdir", tmp_path): + result = await view.get( + request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg" + ) + assert isinstance(result, web.Response) diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 0047cc62365..16a6f5d0f56 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -8,9 +8,10 @@ from systembridgeconnector.exceptions import ( ConnectionErrorException, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.system_bridge.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( FIXTURE_AUTH_INPUT, @@ -32,7 +33,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -42,7 +43,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -67,7 +68,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-bridge" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -79,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -91,7 +92,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -102,7 +103,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -123,7 +124,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -134,7 +135,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -155,7 +156,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -166,7 +167,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -187,7 +188,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -198,7 +199,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -219,7 +220,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -230,7 +231,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -251,7 +252,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -262,7 +263,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with ( @@ -283,7 +284,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "invalid_auth"} @@ -294,7 +295,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with patch( @@ -306,7 +307,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -328,7 +329,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "authenticate" assert result3["errors"] == {"base": "cannot_connect"} @@ -339,7 +340,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with ( @@ -360,7 +361,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -376,7 +377,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with ( @@ -401,7 +402,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -414,7 +415,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with ( @@ -439,7 +440,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == FIXTURE_ZEROCONF_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -454,7 +455,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -466,7 +467,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -480,5 +481,5 @@ async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF_BAD, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/system_bridge/test_init.py b/tests/components/system_bridge/test_init.py index 67d8595ba4c..7632a0c8157 100644 --- a/tests/components/system_bridge/test_init.py +++ b/tests/components/system_bridge/test_init.py @@ -46,7 +46,7 @@ async def test_migration_minor_1_to_2(hass: HomeAssistant) -> None: CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], CONF_TOKEN: FIXTURE_USER_INPUT[CONF_TOKEN], } - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_migration_minor_future_version(hass: HomeAssistant) -> None: @@ -80,4 +80,4 @@ async def test_migration_minor_future_version(hass: HomeAssistant) -> None: 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 + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 5e4eda7d643..e3550101dcc 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -471,10 +471,35 @@ async def test__figure_out_source(hass: HomeAssistant) -> None: file, line_no = system_log._figure_out_source( mock_record, paths_re, - traceback.extract_tb(exc_info[2]), + list(traceback.walk_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) + + +async def test_formatting_exception(hass: HomeAssistant) -> None: + """Test that exceptions are formatted correctly.""" + try: + raise ValueError("test") + except ValueError as ex: + exc_info = (type(ex), ex, ex.__traceback__) + mock_record = MagicMock( + pathname="figure_out_source is False", + lineno=5, + exc_info=exc_info, + exc_text=None, + ) + regex_str = f"({__file__})" + paths_re = re.compile(regex_str) + + mock_formatter = MagicMock( + formatException=MagicMock(return_value="formatted exception") + ) + entry = system_log.LogEntry( + mock_record, paths_re, formatter=mock_formatter, figure_out_source=False + ) + assert entry.exception == "formatted exception" + assert mock_record.exc_text == "formatted exception" diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py index eb6f5778805..bd98099accc 100644 --- a/tests/components/systemmonitor/test_config_flow.py +++ b/tests/components/systemmonitor/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +60,7 @@ async def test_import( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["options"] == { "binary_sensor": {"process": ["systemd", "octave-cli"]}, "resources": [ @@ -99,7 +99,7 @@ async def test_form_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -107,7 +107,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -147,7 +147,7 @@ async def test_import_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" issue = issue_registry.async_get_issue( @@ -179,7 +179,7 @@ async def test_add_and_remove_processes( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -190,7 +190,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["systemd"], @@ -200,7 +200,7 @@ async def test_add_and_remove_processes( # Add another result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -211,7 +211,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["systemd", "octave-cli"], @@ -230,7 +230,7 @@ async def test_add_and_remove_processes( # Remove one result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -241,7 +241,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["systemd"], @@ -251,7 +251,7 @@ async def test_add_and_remove_processes( # Remove last result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -262,7 +262,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": {CONF_PROCESS: []}, } diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index 2142eecb8b4..97f4a41b96c 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -21,7 +21,7 @@ async def test_load_unload_entry( ) -> None: """Test load and unload an entry.""" - assert mock_added_config_entry.state == ConfigEntryState.LOADED + assert mock_added_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(mock_added_config_entry.entry_id) await hass.async_block_till_done() assert mock_added_config_entry.state is ConfigEntryState.NOT_LOADED @@ -38,7 +38,7 @@ async def test_adding_processor_to_options( mock_added_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -49,7 +49,7 @@ async def test_adding_processor_to_options( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["python3", "pip", "systemd"], diff --git a/tests/components/systemmonitor/test_repairs.py b/tests/components/systemmonitor/test_repairs.py index 4c65f531acc..d054bfa99a4 100644 --- a/tests/components/systemmonitor/test_repairs.py +++ b/tests/components/systemmonitor/test_repairs.py @@ -94,7 +94,8 @@ async def test_migrate_process_sensor( assert resp.status == HTTPStatus.OK data = await resp.json() - assert data["type"] == FlowResultType.CREATE_ENTRY + # Cannot use identity `is` check here as the value is parsed from JSON + assert data["type"] == FlowResultType.CREATE_ENTRY.value await hass.async_block_till_done() state = hass.states.get("binary_sensor.system_monitor_process_python3") @@ -192,5 +193,6 @@ async def test_other_fixable_issues( assert resp.status == HTTPStatus.OK data = await resp.json() - assert data["type"] == FlowResultType.CREATE_ENTRY + # Cannot use identity `is` check here as the value is parsed from JSON + assert data["type"] == FlowResultType.CREATE_ENTRY.value await hass.async_block_till_done() diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index c2bbe4f37de..6f44bee8960 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -56,7 +56,7 @@ async def test_form_exceptions( {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} # Test a retry to recover, upon failure @@ -78,7 +78,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "myhome" assert result["data"] == { "username": "test-username", @@ -95,7 +95,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -108,7 +108,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -117,7 +117,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT} @@ -127,7 +127,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) @@ -148,7 +148,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "myhome" assert result["data"] == { "username": "test-username", @@ -176,7 +176,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -199,7 +199,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -220,7 +220,7 @@ async def test_no_homes(hass: HomeAssistant) -> None: {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_homes"} @@ -240,7 +240,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -267,7 +267,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT async def test_import_step(hass: HomeAssistant) -> None: @@ -295,7 +295,7 @@ async def test_import_step(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "username": "test-username", "password": "test-password", @@ -331,7 +331,7 @@ async def test_import_step_existing_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_setup_entry.call_count == 0 @@ -353,7 +353,7 @@ async def test_import_step_validation_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -374,7 +374,7 @@ async def test_import_step_device_authentication_failed(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "import_failed_invalid_auth" @@ -406,6 +406,6 @@ async def test_import_step_unique_id_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_setup_entry.call_count == 0 diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 53c511b8594..a034334508f 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.tag import async_scan_tag from homeassistant.components.tag.const import DEVICE_ID, DOMAIN, TAG_ID from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index 5bf814a56d6..86daa40d8dc 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -23,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "homeassistant.github" assert result2.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -59,7 +59,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "invalid_auth"} @@ -87,7 +87,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "homeassistant.github" assert result3.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -113,7 +113,7 @@ async def test_connection_error( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -137,7 +137,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -146,7 +146,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -179,7 +179,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError @@ -189,7 +189,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} @@ -203,7 +203,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -231,7 +231,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_tailscale_config_flow.devices.side_effect = TailscaleConnectionError @@ -241,6 +241,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/tailwind/test_config_flow.py b/tests/components/tailwind/test_config_flow.py index efd828dcbde..f70ab6e27ff 100644 --- a/tests/components/tailwind/test_config_flow.py +++ b/tests/components/tailwind/test_config_flow.py @@ -40,7 +40,7 @@ async def test_user_flow( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -51,7 +51,7 @@ async def test_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -81,7 +81,7 @@ async def test_user_flow_errors( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == expected_error @@ -93,7 +93,7 @@ async def test_user_flow_errors( CONF_TOKEN: "123456", }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY async def test_user_flow_unsupported_firmware_version( @@ -110,7 +110,7 @@ async def test_user_flow_unsupported_firmware_version( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unsupported_firmware" @@ -134,7 +134,7 @@ async def test_user_flow_already_configured( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" assert mock_config_entry.data[CONF_TOKEN] == "987654" @@ -166,7 +166,7 @@ async def test_zeroconf_flow( ) assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 @@ -176,7 +176,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input={CONF_TOKEN: "987654"} ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -205,7 +205,7 @@ async def test_zeroconf_flow_abort_incompatible_properties( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == expected_reason @@ -252,7 +252,7 @@ async def test_zeroconf_flow_errors( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "zeroconf_confirm" assert result2.get("errors") == expected_error @@ -263,7 +263,7 @@ async def test_zeroconf_flow_errors( CONF_TOKEN: "123456", }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("mock_tailwind") @@ -297,7 +297,7 @@ async def test_zeroconf_flow_not_discovered_again( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" @@ -320,7 +320,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -329,7 +329,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data[CONF_TOKEN] == "987654" @@ -371,7 +371,7 @@ async def test_reauth_flow_errors( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == expected_error @@ -383,7 +383,7 @@ async def test_reauth_flow_errors( }, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" @@ -405,7 +405,7 @@ async def test_dhcp_discovery_updates_entry( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" @@ -425,5 +425,5 @@ async def test_dhcp_discovery_ignores_unknown(hass: HomeAssistant) -> None: ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unknown" diff --git a/tests/components/tami4/test_config_flow.py b/tests/components/tami4/test_config_flow.py index 341e56bec84..cf81b015254 100644 --- a/tests/components/tami4/test_config_flow.py +++ b/tests/components/tami4/test_config_flow.py @@ -20,7 +20,7 @@ async def test_step_user_valid_number( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -28,7 +28,7 @@ async def test_step_user_valid_number( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {} @@ -44,7 +44,7 @@ async def test_step_user_invalid_number( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +52,7 @@ async def test_step_user_invalid_number( result["flow_id"], user_input={CONF_PHONE: "+275123"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_phone"} @@ -74,7 +74,7 @@ async def test_step_user_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -82,7 +82,7 @@ async def test_step_user_exception( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": expected_error} @@ -99,7 +99,7 @@ async def test_step_otp_valid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -107,7 +107,7 @@ async def test_step_otp_valid( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {} @@ -115,7 +115,7 @@ async def test_step_otp_valid( result["flow_id"], user_input={"otp": "123456"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Drink Water" assert "refresh_token" in result["data"] @@ -142,7 +142,7 @@ async def test_step_otp_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -150,7 +150,7 @@ async def test_step_otp_exception( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {} @@ -158,6 +158,6 @@ async def test_step_otp_exception( result["flow_id"], user_input={"otp": "123456"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {"base": expected_error} diff --git a/tests/components/tami4/test_init.py b/tests/components/tami4/test_init.py index 62e5861e13c..2e9663c2728 100644 --- a/tests/components/tami4/test_init.py +++ b/tests/components/tami4/test_init.py @@ -13,7 +13,7 @@ async def test_init_success(mock_api, hass: HomeAssistant) -> None: """Test setup and that we can create the entry.""" entry = await create_config_entry(hass) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -23,7 +23,7 @@ async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None: """Test init with api error.""" entry = await create_config_entry(hass) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( diff --git a/tests/components/tankerkoenig/conftest.py b/tests/components/tankerkoenig/conftest.py index 4400082a45f..1a3dcb6f991 100644 --- a/tests/components/tankerkoenig/conftest.py +++ b/tests/components/tankerkoenig/conftest.py @@ -6,20 +6,11 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.tankerkoenig import DOMAIN -from homeassistant.components.tankerkoenig.const import CONF_FUEL_TYPES, CONF_STATIONS -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LOCATION, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, - CONF_SHOW_ON_MAP, -) +from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import NEARBY_STATIONS, PRICES, STATION +from .const import CONFIG_DATA, NEARBY_STATIONS, PRICES, STATION from tests.common import MockConfigEntry @@ -55,16 +46,7 @@ async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: options={ CONF_SHOW_ON_MAP: True, }, - data={ - CONF_NAME: "Home", - CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", - CONF_FUEL_TYPES: ["e5"], - CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, - CONF_RADIUS: 2.0, - CONF_STATIONS: [ - "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - ], - }, + data=CONFIG_DATA, ) diff --git a/tests/components/tankerkoenig/const.py b/tests/components/tankerkoenig/const.py index 9ec64eb79a9..2c28753a7f3 100644 --- a/tests/components/tankerkoenig/const.py +++ b/tests/components/tankerkoenig/const.py @@ -2,6 +2,16 @@ from aiotankerkoenig import PriceInfo, Station, Status +from homeassistant.components.tankerkoenig.const import CONF_FUEL_TYPES, CONF_STATIONS +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, +) + NEARBY_STATIONS = [ Station( id="3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", @@ -49,6 +59,25 @@ STATION = Station( state="xxXX", ) +STATION_MISSING_FUELTYPE = Station( + id="3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + name="Station ABC", + brand="Station", + street="Somewhere Street", + house_number="1", + post_code=1234, + place="Somewhere", + opening_times=[], + overrides=[], + whole_day=True, + is_open=True, + e5=1.719, + e10=1.659, + lat=51.1, + lng=13.1, + state="xxXX", +) + PRICES = { "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8": PriceInfo( status=Status.OPEN, @@ -57,3 +86,22 @@ PRICES = { diesel=1.659, ), } + +PRICES_MISSING_FUELTYPE = { + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8": PriceInfo( + status=Status.OPEN, + e5=1.719, + e10=1.659, + ), +} + +CONFIG_DATA = { + CONF_NAME: "Home", + CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", + CONF_FUEL_TYPES: ["e5"], + CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, + CONF_RADIUS: 2.0, + CONF_STATIONS: [ + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + ], +} diff --git a/tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr b/tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6b454820b05 --- /dev/null +++ b/tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr @@ -0,0 +1,9 @@ +# serializer version: 1 +# name: test_binary_sensor + ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Station Somewhere Street 1 Status', + 'latitude': 51.1, + 'longitude': 13.1, + }) +# --- diff --git a/tests/components/tankerkoenig/snapshots/test_sensor.ambr b/tests/components/tankerkoenig/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..ec9a72e141d --- /dev/null +++ b/tests/components/tankerkoenig/snapshots/test_sensor.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_sensor + ReadOnlyDict({ + 'attribution': 'Data provided by https://www.tankerkoenig.de', + 'brand': 'Station', + 'city': 'Somewhere', + 'friendly_name': 'Station Somewhere Street 1 Super E10', + 'fuel_type': , + 'house_number': '1', + 'latitude': 51.1, + 'longitude': 13.1, + 'postcode': 1234, + 'state_class': , + 'station_name': 'Station ABC', + 'street': 'Somewhere Street', + 'unit_of_measurement': '€', + }) +# --- +# name: test_sensor.1 + ReadOnlyDict({ + 'attribution': 'Data provided by https://www.tankerkoenig.de', + 'brand': 'Station', + 'city': 'Somewhere', + 'friendly_name': 'Station Somewhere Street 1 Super', + 'fuel_type': , + 'house_number': '1', + 'latitude': 51.1, + 'longitude': 13.1, + 'postcode': 1234, + 'state_class': , + 'station_name': 'Station ABC', + 'street': 'Somewhere Street', + 'unit_of_measurement': '€', + }) +# --- +# name: test_sensor.2 + ReadOnlyDict({ + 'attribution': 'Data provided by https://www.tankerkoenig.de', + 'brand': 'Station', + 'city': 'Somewhere', + 'friendly_name': 'Station Somewhere Street 1 Diesel', + 'fuel_type': , + 'house_number': '1', + 'latitude': 51.1, + 'longitude': 13.1, + 'postcode': 1234, + 'state_class': , + 'station_name': 'Station ABC', + 'street': 'Somewhere Street', + 'unit_of_measurement': '€', + }) +# --- diff --git a/tests/components/tankerkoenig/test_binary_sensor.py b/tests/components/tankerkoenig/test_binary_sensor.py new file mode 100644 index 00000000000..c103f2d26ff --- /dev/null +++ b/tests/components/tankerkoenig/test_binary_sensor.py @@ -0,0 +1,25 @@ +"""Tests for the Tankerkoening integration.""" + +from __future__ import annotations + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("setup_integration") +async def test_binary_sensor( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the tankerkoenig binary sensors.""" + + state = hass.states.get("binary_sensor.station_somewhere_street_1_status") + assert state + assert state.state == STATE_ON + assert state.attributes == snapshot diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index b954598c12a..022b49fd3f8 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for Tankerkoenig config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aiotankerkoenig.exceptions import TankerkoenigInvalidKeyError @@ -21,6 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from .const import NEARBY_STATIONS @@ -56,7 +57,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -71,13 +72,13 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_STATIONS_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -107,14 +108,14 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +124,7 @@ async def test_exception_security(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -133,7 +134,7 @@ async def test_exception_security(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_API_KEY] == "invalid_auth" @@ -143,7 +144,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -153,7 +154,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -176,7 +177,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # re-auth unsuccessful @@ -187,7 +188,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -199,7 +200,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_setup_entry.assert_called() @@ -208,7 +209,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non assert entry.data[CONF_API_KEY] == "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx" -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, tankerkoenig: AsyncMock) -> None: """Test options flow.""" mock_config = MockConfigEntry( @@ -218,13 +219,20 @@ async def test_options_flow(hass: HomeAssistant) -> None: unique_id=f"{DOMAIN}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LATITUDE]}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LONGITUDE]}", ) mock_config.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - with patch( - "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", - return_value=NEARBY_STATIONS, + with ( + patch( + "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", + return_value=NEARBY_STATIONS, + ), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as mock_async_reload, ): result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -234,9 +242,13 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] + await hass.async_block_till_done() + + assert mock_async_reload.call_count == 1 + async def test_options_flow_error(hass: HomeAssistant) -> None: """Test options flow.""" @@ -254,7 +266,7 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: side_effect=TankerkoenigInvalidKeyError("Booom!"), ) as mock_nearby_stations: result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -267,5 +279,5 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] diff --git a/tests/components/tankerkoenig/test_coordinator.py b/tests/components/tankerkoenig/test_coordinator.py index 5a33cb95dd9..3ba0dc31c5f 100644 --- a/tests/components/tankerkoenig/test_coordinator.py +++ b/tests/components/tankerkoenig/test_coordinator.py @@ -13,13 +13,22 @@ from aiotankerkoenig.exceptions import ( ) import pytest -from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.tankerkoenig.const import ( + CONF_STATIONS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import ATTR_ID, CONF_SHOW_ON_MAP, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +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 .const import CONFIG_DATA + from tests.common import MockConfigEntry, async_fire_time_changed @@ -31,7 +40,7 @@ async def test_rate_limit( caplog: pytest.LogCaptureFixture, ) -> None: """Test detection of API rate limit.""" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.station_somewhere_street_1_status") assert state assert state.state == "on" @@ -121,3 +130,104 @@ async def test_setup_exception_logging( await hass.async_block_till_done() assert expected_log in caplog.text + + +async def test_automatic_registry_cleanup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + tankerkoenig: AsyncMock, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test automatic registry cleanup for obsolete entity and devices entries.""" + # setup normal + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 4 + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 1 + ) + + # add obsolete entity and device entries + obsolete_station_id = "aabbccddee-xxxx-xxxx-xxxx-ff11223344" + + entity_registry.async_get_or_create( + DOMAIN, + BINARY_SENSOR_DOMAIN, + f"{obsolete_station_id}_status", + config_entry=config_entry, + ) + entity_registry.async_get_or_create( + DOMAIN, + SENSOR_DOMAIN, + f"{obsolete_station_id}_e10", + config_entry=config_entry, + ) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(ATTR_ID, obsolete_station_id)}, + name="Obsolete Station", + ) + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 6 + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 2 + ) + + # reload config entry to trigger automatic cleanup + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 4 + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 1 + ) + + +async def test_many_stations_warning( + hass: HomeAssistant, tankerkoenig: AsyncMock, caplog: pytest.LogCaptureFixture +) -> None: + """Test the warning about morethan 10 selected stations.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + **CONFIG_DATA, + CONF_STATIONS: [ + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "36b4b812-xxxx-xxxx-xxxx-c51735325858", + "54e2b642-xxxx-xxxx-xxxx-87cd4e9867f1", + "11b5c130-xxxx-xxxx-xxxx-856b8489b528", + "a9137924-xxxx-xxxx-xxxx-7029d7eb073f", + "57c6d275-xxxx-xxxx-xxxx-7f6ad9e6d638", + "bbc3c3a2-xxxx-xxxx-xxxx-840cc3d496b6", + "1db63dd9-xxxx-xxxx-xxxx-a889b53cbc65", + "18d7262e-xxxx-xxxx-xxxx-4a61ad302e14", + "a8041aa3-xxxx-xxxx-xxxx-7c6b180e5a40", + "739aa0eb-xxxx-xxxx-xxxx-a3d7b6c8a42f", + "9ad9fb26-xxxx-xxxx-xxxx-84e6a02b3096", + "74267867-xxxx-xxxx-xxxx-74ce3d45882c", + "86657222-xxxx-xxxx-xxxx-a2b795ab3cf9", + ], + }, + options={CONF_SHOW_ON_MAP: True}, + unique_id="51.0_13.0", + ) + mock_config.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert "Found more than 10 stations to check" in caplog.text diff --git a/tests/components/tankerkoenig/test_sensor.py b/tests/components/tankerkoenig/test_sensor.py new file mode 100644 index 00000000000..788c1de7021 --- /dev/null +++ b/tests/components/tankerkoenig/test_sensor.py @@ -0,0 +1,65 @@ +"""Tests for the Tankerkoening integration.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.tankerkoenig import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import PRICES_MISSING_FUELTYPE, STATION_MISSING_FUELTYPE + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("setup_integration") +async def test_sensor( + hass: HomeAssistant, + tankerkoenig: AsyncMock, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the tankerkoenig sensors.""" + + state = hass.states.get("sensor.station_somewhere_street_1_super_e10") + assert state + assert state.state == "1.659" + assert state.attributes == snapshot + + state = hass.states.get("sensor.station_somewhere_street_1_super") + assert state + assert state.state == "1.719" + assert state.attributes == snapshot + + state = hass.states.get("sensor.station_somewhere_street_1_diesel") + assert state + assert state.state == "1.659" + assert state.attributes == snapshot + + +async def test_sensor_missing_fueltype( + hass: HomeAssistant, + tankerkoenig: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test the tankerkoenig sensors.""" + tankerkoenig.station_details.return_value = STATION_MISSING_FUELTYPE + tankerkoenig.prices.return_value = PRICES_MISSING_FUELTYPE + + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.station_somewhere_street_1_super_e10") + assert state + + state = hass.states.get("sensor.station_somewhere_street_1_super") + assert state + + state = hass.states.get("sensor.station_somewhere_street_1_diesel") + assert not state diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 360794e280f..499e732719c 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -163,7 +163,7 @@ async def help_test_availability_when_connection_lost( # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False - await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -172,7 +172,7 @@ async def help_test_availability_when_connection_lost( # Reconnected to MQTT server -> state still unavailable mqtt_mock.connected = True - await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -224,7 +224,7 @@ async def help_test_deep_sleep_availability_when_connection_lost( # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False - await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -233,7 +233,7 @@ async def help_test_deep_sleep_availability_when_connection_lost( # Reconnected to MQTT server -> state no longer unavailable mqtt_mock.connected = True - await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -476,7 +476,7 @@ async def help_test_availability_poll_state( # Disconnected from MQTT server mqtt_mock.connected = False - await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -484,7 +484,7 @@ async def help_test_availability_poll_state( # Reconnected to MQTT server mqtt_mock.connected = True - await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 2db4d7c6493..4d5b655a9c9 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -2,6 +2,7 @@ from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ async def test_mqtt_abort_if_existing_entry( "tasmota", context={"source": config_entries.SOURCE_MQTT} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -46,7 +47,7 @@ async def test_mqtt_abort_invalid_topic( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" discovery_info = MqttServiceInfo( @@ -60,7 +61,7 @@ async def test_mqtt_abort_invalid_topic( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" discovery_info = MqttServiceInfo( @@ -83,7 +84,7 @@ async def test_mqtt_abort_invalid_topic( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: @@ -108,11 +109,11 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == {"discovery_prefix": "tasmota/discovery"} @@ -121,11 +122,11 @@ async def test_user_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "discovery_prefix": "tasmota/discovery", } @@ -139,13 +140,13 @@ async def test_user_setup_advanced( "tasmota", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery_prefix": "test_tasmota/discovery"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "discovery_prefix": "test_tasmota/discovery", } @@ -159,13 +160,13 @@ async def test_user_setup_advanced_strip_wildcard( "tasmota", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery_prefix": "test_tasmota/discovery/#"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "discovery_prefix": "test_tasmota/discovery", } @@ -179,13 +180,13 @@ async def test_user_setup_invalid_topic_prefix( "tasmota", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery_prefix": "tasmota/config/##"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "invalid_discovery_topic" @@ -198,5 +199,5 @@ async def test_user_single_instance( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index a5d30814b38..8d299a272f7 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -8,7 +8,7 @@ from hatasmota.switch import TasmotaSwitchTriggerConfig import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.tasmota import _LOGGER from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN diff --git a/tests/components/tautulli/__init__.py b/tests/components/tautulli/__init__.py index b48488c7216..8ca0b13d198 100644 --- a/tests/components/tautulli/__init__.py +++ b/tests/components/tautulli/__init__.py @@ -72,7 +72,7 @@ async def setup_integration( aioclient_mock: AiohttpClientMocker, url: str = URL, api_key: str = API_KEY, - unique_id: str = None, + unique_id: str | None = None, skip_entry_setup: bool = False, invalid_auth: bool = False, ) -> MockConfigEntry: diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index e51fbfbad0d..ca563cfad77 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -21,7 +21,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -32,7 +32,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -44,7 +44,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -55,7 +55,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -67,7 +67,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -78,7 +78,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -90,7 +90,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -101,7 +101,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -117,7 +117,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -129,11 +129,11 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} - input = { + user_input = { CONF_URL: "http://1.2.3.5:8181/test", CONF_API_KEY: "efgh", CONF_VERIFY_SSL: True, @@ -141,13 +141,13 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: with patch_config_flow_tautulli(AsyncMock()): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=input, + user_input=user_input, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME - assert result2["data"] == input + assert result2["data"] == user_input async def test_flow_reauth( @@ -165,7 +165,7 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -181,7 +181,7 @@ async def test_flow_reauth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == CONF_DATA assert len(mock_entry.mock_calls) == 1 @@ -207,7 +207,7 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -216,5 +216,5 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/technove/test_config_flow.py b/tests/components/technove/test_config_flow.py index 72b9b358c89..81e0b32b55b 100644 --- a/tests/components/technove/test_config_flow.py +++ b/tests/components/technove/test_config_flow.py @@ -25,14 +25,14 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) assert result.get("title") == "TechnoVE Station" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -53,7 +53,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -66,7 +66,7 @@ async def test_connection_error(hass: HomeAssistant, mock_technove: MagicMock) - data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -83,13 +83,13 @@ async def test_full_user_flow_with_error( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -99,7 +99,7 @@ async def test_full_user_flow_with_error( ) assert result.get("title") == "TechnoVE Station" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -128,14 +128,14 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: assert result.get("description_placeholders") == {CONF_NAME: "TechnoVE Station"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2.get("title") == "TechnoVE Station" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -165,7 +165,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "TechnoVE Station" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -195,7 +195,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -211,7 +211,7 @@ async def test_user_station_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -236,7 +236,7 @@ async def test_zeroconf_without_mac_station_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -262,5 +262,5 @@ async def test_zeroconf_with_mac_station_exists_abort( ) mock_technove.update.assert_not_called() - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index 6e8f02d04bc..1da1e392bf3 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -27,7 +27,7 @@ async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -37,7 +37,7 @@ async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: "192.168.1.62", CONF_LOCAL_ACCESS_TOKEN: "token", @@ -56,7 +56,7 @@ async def test_flow_already_configured( DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -65,7 +65,7 @@ async def test_flow_already_configured( CONF_LOCAL_ACCESS_TOKEN: "token", }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_config_flow_errors( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM mock_tedee.get_local_bridge.side_effect = side_effect @@ -103,7 +103,7 @@ async def test_config_flow_errors( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error assert len(mock_tedee.get_local_bridge.mock_calls) == 1 @@ -134,5 +134,5 @@ async def test_reauth_flow( CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py index ee13d8dc47c..e1daf4da074 100644 --- a/tests/components/telegram/test_notify.py +++ b/tests/components/telegram/test_notify.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant import config as hass_config -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.telegram import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 3cd157fd8b5..c575e7fb5c1 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.tellduslive import ( from homeassistant.config_entries import SOURCE_DISCOVERY from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -63,12 +64,12 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -77,16 +78,16 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_tellduslive) - flow = init_config_flow(hass) flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert len(flow._hosts) == 2 result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"host": "localhost"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "auth_url": "https://example.com", @@ -94,7 +95,7 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_tellduslive) - } result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -106,7 +107,7 @@ async def test_step_import(hass: HomeAssistant, mock_tellduslive) -> None: flow = init_config_flow(hass) result = await flow.async_step_import({CONF_HOST: DOMAIN, KEY_SCAN_INTERVAL: 0}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -117,7 +118,7 @@ async def test_step_import_add_host(hass: HomeAssistant, mock_tellduslive) -> No result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -130,7 +131,7 @@ async def test_step_import_no_config_file( result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -150,7 +151,7 @@ async def test_step_import_load_json_matching_host( result = await flow.async_step_import( {CONF_HOST: "Cloud API", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -168,7 +169,7 @@ async def test_step_import_load_json(hass: HomeAssistant, mock_tellduslive) -> N result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -182,7 +183,7 @@ async def test_step_disco_no_local_api(hass: HomeAssistant, mock_tellduslive) -> flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert len(flow._hosts) == 1 @@ -193,7 +194,7 @@ async def test_step_auth(hass: HomeAssistant, mock_tellduslive) -> None: await flow.async_step_auth() result = await flow.async_step_auth(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API" assert result["data"]["host"] == "Cloud API" assert result["data"]["scan_interval"] == 60 @@ -212,7 +213,7 @@ async def test_wrong_auth_flow_implementation( await flow.async_step_auth() result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"]["base"] == "invalid_auth" @@ -222,7 +223,7 @@ async def test_not_pick_host_if_only_one(hass: HomeAssistant, mock_tellduslive) flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -233,7 +234,7 @@ async def test_abort_if_timeout_generating_auth_url( flow = init_config_flow(hass, side_effect=TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -243,7 +244,7 @@ async def test_abort_no_auth_url(hass: HomeAssistant, mock_tellduslive) -> None: flow._get_auth_url = Mock(return_value=False) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -254,7 +255,7 @@ async def test_abort_if_exception_generating_auth_url( flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 30d6942750c..8c5dda401dd 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -8,6 +8,7 @@ from pytest_unordered import unordered from homeassistant import config_entries from homeassistant.components.template import DOMAIN, async_setup_entry +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -72,14 +73,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type with patch( @@ -95,7 +96,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My template" assert result["data"] == {} assert result["options"] == { @@ -203,7 +204,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert get_suggested(result["data_schema"].schema, "state") == old_state_template assert "name" not in result["data_schema"].schema @@ -212,7 +213,7 @@ async def test_options( result["flow_id"], user_input={"state": new_state_template, **options_options}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My template", "state": new_state_template, @@ -237,14 +238,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert get_suggested(result["data_schema"].schema, "name") is None @@ -276,7 +277,7 @@ async def test_options( "{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}", {}, {"one": "30.0", "two": "20.0"}, - ["", "50.0"], + ["", STATE_UNAVAILABLE, "50.0"], [{}, {}], [["one", "two"], ["one", "two"]], ), @@ -301,14 +302,14 @@ async def test_config_flow_preview( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -342,20 +343,24 @@ async def test_config_flow_preview( hass.states.async_set( f"{template_type}.{input_entity}", input_states[input_entity], {} ) + await hass.async_block_till_done() - msg = await client.receive_json() - assert msg["event"] == { - "attributes": {"friendly_name": "My template"} - | extra_attributes[0] - | extra_attributes[1], - "listeners": { - "all": False, - "domains": [], - "entities": unordered([f"{template_type}.{_id}" for _id in listeners[1]]), - "time": False, - }, - "state": template_states[1], - } + for template_state in template_states[1:]: + msg = await client.receive_json() + assert msg["event"] == { + "attributes": {"friendly_name": "My template"} + | extra_attributes[0] + | extra_attributes[1], + "listeners": { + "all": False, + "domains": [], + "entities": unordered( + [f"{template_type}.{_id}" for _id in listeners[1]] + ), + "time": False, + }, + "state": template_state, + } assert len(hass.states.async_all()) == 2 @@ -442,14 +447,14 @@ async def test_config_flow_preview_bad_input( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -512,14 +517,14 @@ async def test_config_flow_preview_template_startup_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -595,14 +600,14 @@ async def test_config_flow_preview_template_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -665,14 +670,14 @@ async def test_config_flow_preview_bad_state( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": template_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -754,7 +759,7 @@ async def test_option_flow_preview( """Test the option flow preview.""" client = await hass_ws_client(hass) - input_entities = input_entities = ["one", "two"] + input_entities = ["one", "two"] # Setup the config entry config_entry = MockConfigEntry( @@ -773,7 +778,7 @@ async def test_option_flow_preview( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "template" @@ -830,7 +835,7 @@ async def test_option_flow_sensor_preview_config_entry_removed( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "template" diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index a40f093a573..0dfbc0f833d 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.light as light +from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index db7cd3a2471..0f95503c333 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -6,7 +6,7 @@ from unittest import mock from freezegun.api import FrozenDateTimeFactory import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.template import trigger as template_trigger from homeassistant.const import ( ATTR_ENTITY_ID, @@ -329,15 +329,12 @@ async def test_if_not_fires_because_fail( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -430,15 +427,12 @@ async def test_if_fires_on_change_with_bad_template( { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -502,15 +496,12 @@ async def test_if_fires_on_change_with_for(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -549,15 +540,12 @@ async def test_if_fires_on_change_with_for_advanced( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -593,15 +581,12 @@ async def test_if_fires_on_change_with_for_0_advanced( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index a31df11dcd0..e10ae190a59 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -88,14 +88,12 @@ async def create_wall_connector_entry( def get_vitals_mock() -> Vitals: """Get mocked vitals object.""" - vitals = MagicMock(auto_spec=Vitals) - return vitals + return MagicMock(auto_spec=Vitals) def get_lifetime_mock() -> Lifetime: """Get mocked lifetime object.""" - lifetime = MagicMock(auto_spec=Lifetime) - return lifetime + return MagicMock(auto_spec=Lifetime) @dataclass diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 198dcccfe00..a0c28262658 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tesla Wall Connector" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,7 +74,7 @@ async def test_form_other_error( {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -98,7 +98,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -120,7 +120,7 @@ async def test_dhcp_can_finish( ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -129,7 +129,7 @@ async def test_dhcp_can_finish( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: "1.2.3.4"} @@ -154,7 +154,7 @@ async def test_dhcp_already_exists( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -178,5 +178,5 @@ async def test_dhcp_error_from_wall_connector( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/tesla_wall_connector/test_init.py b/tests/components/tesla_wall_connector/test_init.py index 152bf18d57e..2b37924b2e4 100644 --- a/tests/components/tesla_wall_connector/test_init.py +++ b/tests/components/tesla_wall_connector/test_init.py @@ -2,7 +2,7 @@ from tesla_wall_connector.exceptions import WallConnectorConnectionError -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import create_wall_connector_entry @@ -13,7 +13,7 @@ async def test_init_success(hass: HomeAssistant) -> None: entry = await create_wall_connector_entry(hass) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_init_while_offline(hass: HomeAssistant) -> None: @@ -22,7 +22,7 @@ async def test_init_while_offline(hass: HomeAssistant) -> None: hass, side_effect=WallConnectorConnectionError ) - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_load_unload(hass: HomeAssistant) -> None: @@ -30,7 +30,7 @@ async def test_load_unload(hass: HomeAssistant) -> None: entry = await create_wall_connector_entry(hass) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/teslemetry/conftest.py b/tests/components/teslemetry/conftest.py index f252787b37c..9040ec96a03 100644 --- a/tests/components/teslemetry/conftest.py +++ b/tests/components/teslemetry/conftest.py @@ -7,7 +7,23 @@ from unittest.mock import patch import pytest -from .const import LIVE_STATUS, PRODUCTS, RESPONSE_OK, VEHICLE_DATA, WAKE_UP_ONLINE +from .const import ( + LIVE_STATUS, + METADATA, + PRODUCTS, + RESPONSE_OK, + VEHICLE_DATA, + WAKE_UP_ONLINE, +) + + +@pytest.fixture(autouse=True) +def mock_metadata(): + """Mock Tesla Fleet Api metadata method.""" + with patch( + "homeassistant.components.teslemetry.Teslemetry.metadata", return_value=METADATA + ) as mock_products: + yield mock_products @pytest.fixture(autouse=True) diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py index 776cc231a5c..96e9ead8912 100644 --- a/tests/components/teslemetry/const.py +++ b/tests/components/teslemetry/const.py @@ -16,3 +16,21 @@ 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} + +METADATA = { + "region": "NA", + "scopes": [ + "openid", + "offline_access", + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds", + ], +} +METADATA_NOSCOPE = { + "region": "NA", + "scopes": ["openid", "offline_access", "vehicle_device_data"], +} diff --git a/tests/components/teslemetry/snapshots/test_diagnostics.ambr b/tests/components/teslemetry/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..74eff27c4a0 --- /dev/null +++ b/tests/components/teslemetry/snapshots/test_diagnostics.ambr @@ -0,0 +1,295 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'energysites': list([ + dict({ + 'backup_capable': True, + 'battery_power': 5060, + 'energy_left': 38896.47368421053, + 'generator_power': 0, + 'grid_power': 0, + 'grid_services_active': False, + 'grid_services_power': 0, + 'grid_status': 'Active', + 'island_status': 'on_grid', + 'load_power': 6245, + 'percentage_charged': 95.50537403739663, + 'solar_power': 1185, + 'storm_mode_active': False, + 'timestamp': '2024-01-01T00:00:00+00:00', + 'total_pack_energy': 40727, + 'wall_connectors': dict({ + 'abd-123': dict({ + 'din': 'abd-123', + 'wall_connector_fault_state': 2, + 'wall_connector_power': 0, + 'wall_connector_state': 2, + }), + 'bcd-234': dict({ + 'din': 'bcd-234', + 'wall_connector_fault_state': 2, + 'wall_connector_power': 0, + 'wall_connector_state': 2, + }), + }), + }), + ]), + 'vehicles': list([ + dict({ + 'access_type': 'OWNER', + 'api_version': 71, + 'backseat_token': None, + 'backseat_token_updated_at': None, + 'ble_autopair_enrolled': False, + 'calendar_enabled': True, + 'charge_state_battery_heater_on': False, + 'charge_state_battery_level': 77, + 'charge_state_battery_range': 266.87, + 'charge_state_charge_amps': 16, + 'charge_state_charge_current_request': 16, + 'charge_state_charge_current_request_max': 16, + 'charge_state_charge_enable_request': True, + 'charge_state_charge_energy_added': 0, + 'charge_state_charge_limit_soc': 80, + 'charge_state_charge_limit_soc_max': 100, + 'charge_state_charge_limit_soc_min': 50, + 'charge_state_charge_limit_soc_std': 80, + 'charge_state_charge_miles_added_ideal': 0, + 'charge_state_charge_miles_added_rated': 0, + 'charge_state_charge_port_cold_weather_mode': False, + 'charge_state_charge_port_color': '', + 'charge_state_charge_port_door_open': True, + 'charge_state_charge_port_latch': 'Engaged', + 'charge_state_charge_rate': 0, + 'charge_state_charger_actual_current': 0, + 'charge_state_charger_phases': None, + 'charge_state_charger_pilot_current': 16, + 'charge_state_charger_power': 0, + 'charge_state_charger_voltage': 2, + 'charge_state_charging_state': 'Stopped', + 'charge_state_conn_charge_cable': 'IEC', + 'charge_state_est_battery_range': 275.04, + 'charge_state_fast_charger_brand': '', + 'charge_state_fast_charger_present': False, + 'charge_state_fast_charger_type': 'ACSingleWireCAN', + 'charge_state_ideal_battery_range': 266.87, + 'charge_state_max_range_charge_counter': 0, + 'charge_state_minutes_to_full_charge': 0, + 'charge_state_not_enough_power_to_heat': None, + 'charge_state_off_peak_charging_enabled': False, + 'charge_state_off_peak_charging_times': 'all_week', + 'charge_state_off_peak_hours_end_time': 900, + 'charge_state_preconditioning_enabled': False, + 'charge_state_preconditioning_times': 'all_week', + 'charge_state_scheduled_charging_mode': 'Off', + 'charge_state_scheduled_charging_pending': False, + 'charge_state_scheduled_charging_start_time': None, + 'charge_state_scheduled_charging_start_time_app': 600, + 'charge_state_scheduled_departure_time': 1704837600, + 'charge_state_scheduled_departure_time_minutes': 480, + 'charge_state_supercharger_session_trip_planner': False, + 'charge_state_time_to_full_charge': 0, + 'charge_state_timestamp': 1705707520649, + 'charge_state_trip_charging': False, + 'charge_state_usable_battery_level': 77, + 'charge_state_user_charge_enable_request': None, + 'climate_state_allow_cabin_overheat_protection': True, + 'climate_state_auto_seat_climate_left': False, + 'climate_state_auto_seat_climate_right': True, + 'climate_state_auto_steering_wheel_heat': False, + 'climate_state_battery_heater': False, + 'climate_state_battery_heater_no_power': None, + 'climate_state_cabin_overheat_protection': 'On', + 'climate_state_cabin_overheat_protection_actively_cooling': False, + 'climate_state_climate_keeper_mode': 'off', + 'climate_state_cop_activation_temperature': 'High', + 'climate_state_defrost_mode': 0, + 'climate_state_driver_temp_setting': 22, + 'climate_state_fan_status': 0, + 'climate_state_hvac_auto_request': 'On', + 'climate_state_inside_temp': 29.8, + 'climate_state_is_auto_conditioning_on': False, + 'climate_state_is_climate_on': False, + 'climate_state_is_front_defroster_on': False, + 'climate_state_is_preconditioning': False, + 'climate_state_is_rear_defroster_on': False, + 'climate_state_left_temp_direction': 251, + 'climate_state_max_avail_temp': 28, + 'climate_state_min_avail_temp': 15, + 'climate_state_outside_temp': 30, + 'climate_state_passenger_temp_setting': 22, + 'climate_state_remote_heater_control_enabled': False, + 'climate_state_right_temp_direction': 251, + 'climate_state_seat_heater_left': 0, + 'climate_state_seat_heater_rear_center': 0, + 'climate_state_seat_heater_rear_left': 0, + 'climate_state_seat_heater_rear_right': 0, + 'climate_state_seat_heater_right': 0, + 'climate_state_side_mirror_heaters': False, + 'climate_state_steering_wheel_heat_level': 0, + 'climate_state_steering_wheel_heater': False, + 'climate_state_supports_fan_only_cabin_overheat_protection': True, + 'climate_state_timestamp': 1705707520649, + 'climate_state_wiper_blade_heater': False, + 'color': None, + 'drive_state_active_route_latitude': '**REDACTED**', + 'drive_state_active_route_longitude': '**REDACTED**', + 'drive_state_active_route_miles_to_arrival': 0.039491, + 'drive_state_active_route_minutes_to_arrival': 0.103577, + 'drive_state_active_route_traffic_minutes_delay': 0, + 'drive_state_gps_as_of': 1701129612, + 'drive_state_heading': 185, + 'drive_state_latitude': '**REDACTED**', + 'drive_state_longitude': '**REDACTED**', + 'drive_state_native_latitude': '**REDACTED**', + 'drive_state_native_location_supported': 1, + 'drive_state_native_longitude': '**REDACTED**', + 'drive_state_native_type': 'wgs', + 'drive_state_power': -7, + 'drive_state_shift_state': None, + 'drive_state_speed': None, + 'drive_state_timestamp': 1705707520649, + 'granular_access_hide_private': False, + 'gui_settings_gui_24_hour_time': False, + 'gui_settings_gui_charge_rate_units': 'kW', + 'gui_settings_gui_distance_units': 'km/hr', + 'gui_settings_gui_range_display': 'Rated', + 'gui_settings_gui_temperature_units': 'C', + 'gui_settings_gui_tirepressure_units': 'Psi', + 'gui_settings_show_range_units': False, + 'gui_settings_timestamp': 1705707520649, + 'id': '**REDACTED**', + 'id_s': '**REDACTED**', + 'in_service': False, + 'state': 'online', + 'tokens': '**REDACTED**', + 'user_id': '**REDACTED**', + 'vehicle_config_aux_park_lamps': 'Eu', + 'vehicle_config_badge_version': 1, + 'vehicle_config_can_accept_navigation_requests': True, + 'vehicle_config_can_actuate_trunks': True, + 'vehicle_config_car_special_type': 'base', + 'vehicle_config_car_type': 'model3', + 'vehicle_config_charge_port_type': 'CCS', + 'vehicle_config_cop_user_set_temp_supported': False, + 'vehicle_config_dashcam_clip_save_supported': True, + 'vehicle_config_default_charge_to_max': False, + 'vehicle_config_driver_assist': 'TeslaAP3', + 'vehicle_config_ece_restrictions': False, + 'vehicle_config_efficiency_package': 'M32021', + 'vehicle_config_eu_vehicle': True, + 'vehicle_config_exterior_color': 'DeepBlue', + 'vehicle_config_exterior_trim': 'Black', + 'vehicle_config_exterior_trim_override': '', + 'vehicle_config_has_air_suspension': False, + 'vehicle_config_has_ludicrous_mode': False, + 'vehicle_config_has_seat_cooling': False, + 'vehicle_config_headlamp_type': 'Global', + 'vehicle_config_interior_trim_type': 'White2', + 'vehicle_config_key_version': 2, + 'vehicle_config_motorized_charge_port': True, + 'vehicle_config_paint_color_override': '0,9,25,0.7,0.04', + 'vehicle_config_performance_package': 'Base', + 'vehicle_config_plg': True, + 'vehicle_config_pws': True, + 'vehicle_config_rear_drive_unit': 'PM216MOSFET', + 'vehicle_config_rear_seat_heaters': 1, + 'vehicle_config_rear_seat_type': 0, + 'vehicle_config_rhd': True, + 'vehicle_config_roof_color': 'RoofColorGlass', + 'vehicle_config_seat_type': None, + 'vehicle_config_spoiler_type': 'None', + 'vehicle_config_sun_roof_installed': None, + 'vehicle_config_supports_qr_pairing': False, + 'vehicle_config_third_row_seats': 'None', + 'vehicle_config_timestamp': 1705707520649, + 'vehicle_config_trim_badging': '74d', + 'vehicle_config_use_range_badging': True, + 'vehicle_config_utc_offset': 36000, + 'vehicle_config_webcam_selfie_supported': True, + 'vehicle_config_webcam_supported': True, + 'vehicle_config_wheel_type': 'Pinwheel18CapKit', + 'vehicle_id': '**REDACTED**', + 'vehicle_state_api_version': 71, + 'vehicle_state_autopark_state_v2': 'unavailable', + 'vehicle_state_calendar_supported': True, + 'vehicle_state_car_version': '2023.44.30.8 06f534d46010', + 'vehicle_state_center_display_state': 0, + 'vehicle_state_dashcam_clip_save_available': True, + 'vehicle_state_dashcam_state': 'Recording', + 'vehicle_state_df': 0, + 'vehicle_state_dr': 0, + 'vehicle_state_fd_window': 0, + 'vehicle_state_feature_bitmask': 'fbdffbff,187f', + 'vehicle_state_fp_window': 0, + 'vehicle_state_ft': 0, + 'vehicle_state_is_user_present': False, + 'vehicle_state_locked': False, + 'vehicle_state_media_info_audio_volume': 2.6667, + 'vehicle_state_media_info_audio_volume_increment': 0.333333, + 'vehicle_state_media_info_audio_volume_max': 10.333333, + 'vehicle_state_media_info_media_playback_status': 'Stopped', + 'vehicle_state_media_info_now_playing_album': '', + 'vehicle_state_media_info_now_playing_artist': '', + 'vehicle_state_media_info_now_playing_duration': 0, + 'vehicle_state_media_info_now_playing_elapsed': 0, + 'vehicle_state_media_info_now_playing_source': 'Spotify', + 'vehicle_state_media_info_now_playing_station': '', + 'vehicle_state_media_info_now_playing_title': '', + 'vehicle_state_media_state_remote_control_enabled': True, + 'vehicle_state_notifications_supported': True, + 'vehicle_state_odometer': 6481.019282, + 'vehicle_state_parsed_calendar_supported': True, + 'vehicle_state_pf': 0, + 'vehicle_state_pr': 0, + 'vehicle_state_rd_window': 0, + 'vehicle_state_remote_start': False, + 'vehicle_state_remote_start_enabled': True, + 'vehicle_state_remote_start_supported': True, + 'vehicle_state_rp_window': 0, + 'vehicle_state_rt': 0, + 'vehicle_state_santa_mode': 0, + 'vehicle_state_sentry_mode': False, + 'vehicle_state_sentry_mode_available': True, + 'vehicle_state_service_mode': False, + 'vehicle_state_service_mode_plus': False, + 'vehicle_state_software_update_download_perc': 0, + 'vehicle_state_software_update_expected_duration_sec': 2700, + 'vehicle_state_software_update_install_perc': 1, + 'vehicle_state_software_update_status': '', + 'vehicle_state_software_update_version': ' ', + 'vehicle_state_speed_limit_mode_active': False, + 'vehicle_state_speed_limit_mode_current_limit_mph': 69, + 'vehicle_state_speed_limit_mode_max_limit_mph': 120, + 'vehicle_state_speed_limit_mode_min_limit_mph': 50, + 'vehicle_state_speed_limit_mode_pin_code_set': True, + 'vehicle_state_timestamp': 1705707520649, + 'vehicle_state_tpms_hard_warning_fl': False, + 'vehicle_state_tpms_hard_warning_fr': False, + 'vehicle_state_tpms_hard_warning_rl': False, + 'vehicle_state_tpms_hard_warning_rr': False, + 'vehicle_state_tpms_last_seen_pressure_time_fl': 1705700812, + 'vehicle_state_tpms_last_seen_pressure_time_fr': 1705700793, + 'vehicle_state_tpms_last_seen_pressure_time_rl': 1705700794, + 'vehicle_state_tpms_last_seen_pressure_time_rr': 1705700823, + 'vehicle_state_tpms_pressure_fl': 2.775, + 'vehicle_state_tpms_pressure_fr': 2.8, + 'vehicle_state_tpms_pressure_rl': 2.775, + 'vehicle_state_tpms_pressure_rr': 2.775, + 'vehicle_state_tpms_rcp_front_value': 2.9, + 'vehicle_state_tpms_rcp_rear_value': 2.9, + 'vehicle_state_tpms_soft_warning_fl': False, + 'vehicle_state_tpms_soft_warning_fr': False, + 'vehicle_state_tpms_soft_warning_rl': False, + 'vehicle_state_tpms_soft_warning_rr': False, + 'vehicle_state_valet_mode': False, + 'vehicle_state_valet_pin_needed': False, + 'vehicle_state_vehicle_name': 'Test', + 'vehicle_state_vehicle_self_test_progress': 0, + 'vehicle_state_vehicle_self_test_requested': False, + 'vehicle_state_webcam_available': True, + 'vin': '**REDACTED**', + }), + ]), + }) +# --- diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index e83e9d648cd..a05bc07b305 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -22,11 +22,11 @@ from homeassistant.components.climate import ( from homeassistant.components.teslemetry.coordinator import SYNC_INTERVAL from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from . import assert_entities, setup_platform -from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE +from .const import METADATA_NOSCOPE, WAKE_UP_ASLEEP, WAKE_UP_ONLINE from tests.common import async_fire_time_changed @@ -176,3 +176,30 @@ async def test_asleep_or_offline( ) await hass.async_block_till_done() mock_wake_up.assert_called_once() + + +async def test_climate_noscope( + hass: HomeAssistant, + mock_metadata, +) -> None: + """Tests that the climate entity is correct.""" + mock_metadata.return_value = METADATA_NOSCOPE + + await setup_platform(hass, [Platform.CLIMATE]) + entity_id = "climate.test_climate" + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20}, + blocking=True, + ) diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index 3757c331996..2f12b202712 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -18,6 +18,10 @@ from homeassistant.data_entry_flow import FlowResultType from .const import CONFIG +from tests.common import MockConfigEntry + +BAD_CONFIG = {CONF_ACCESS_TOKEN: "bad_access_token"} + @pytest.fixture(autouse=True) def mock_test(): @@ -36,7 +40,7 @@ async def test_form( result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert not result1["errors"] with patch( @@ -50,7 +54,7 @@ async def test_form( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == CONFIG @@ -76,7 +80,7 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - CONFIG, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error # Complete the flow @@ -85,4 +89,96 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - result2["flow_id"], CONFIG, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY + + +async def test_reauth(hass: HomeAssistant, mock_test) -> None: + """Test reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=BAD_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + data=BAD_CONFIG, + ) + + assert result1["type"] is FlowResultType.FORM + assert result1["step_id"] == "reauth_confirm" + assert not result1["errors"] + + with patch( + "homeassistant.components.teslemetry.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_test.mock_calls) == 1 + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == CONFIG + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (InvalidToken, {CONF_ACCESS_TOKEN: "invalid_access_token"}), + (SubscriptionRequired, {"base": "subscription_required"}), + (ClientConnectionError, {"base": "cannot_connect"}), + (TeslaFleetError, {"base": "unknown"}), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, mock_test, side_effect, error +) -> None: + """Test reauth flows that fail.""" + + # Start the reauth + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=BAD_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=BAD_CONFIG, + ) + + mock_test.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + BAD_CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == error + + # Complete the flow + mock_test.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + CONFIG, + ) + assert "errors" not in result3 + assert result3["type"] is FlowResultType.ABORT + assert result3["reason"] == "reauth_successful" + assert mock_entry.data == CONFIG diff --git a/tests/components/teslemetry/test_diagnostics.py b/tests/components/teslemetry/test_diagnostics.py new file mode 100644 index 00000000000..fb8eb79a918 --- /dev/null +++ b/tests/components/teslemetry/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Test the Telemetry Diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_platform + +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 setup_platform(hass) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == snapshot diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 9742338f27a..f21a421ed6e 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory +import pytest from tesla_fleet_api.exceptions import ( InvalidToken, SubscriptionRequired, @@ -20,6 +21,12 @@ from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE from tests.common import async_fire_time_changed +ERRORS = [ + (InvalidToken, ConfigEntryState.SETUP_ERROR), + (SubscriptionRequired, ConfigEntryState.SETUP_ERROR), + (TeslaFleetError, ConfigEntryState.SETUP_RETRY), +] + async def test_load_unload(hass: HomeAssistant) -> None: """Test load and unload.""" @@ -31,28 +38,15 @@ async def test_load_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED -async def test_auth_failure(hass: HomeAssistant, mock_products) -> None: - """Test init with an authentication error.""" +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_init_error( + hass: HomeAssistant, mock_products, side_effect, state +) -> None: + """Test init with errors.""" - mock_products.side_effect = InvalidToken + mock_products.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_subscription_failure(hass: HomeAssistant, mock_products) -> None: - """Test init with an client response error.""" - - mock_products.side_effect = SubscriptionRequired - entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_other_failure(hass: HomeAssistant, mock_products) -> None: - """Test init with an client response error.""" - - mock_products.side_effect = TeslaFleetError - entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state # Vehicle Coordinator @@ -80,7 +74,7 @@ async def test_vehicle_first_refresh( # Wait for the retry freezer.tick(timedelta(seconds=60)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Verify we have loaded assert entry.state is ConfigEntryState.LOADED @@ -88,11 +82,14 @@ async def test_vehicle_first_refresh( mock_vehicle_data.assert_called_once() -async def test_vehicle_first_refresh_error(hass: HomeAssistant, mock_wake_up) -> None: +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_vehicle_first_refresh_error( + hass: HomeAssistant, mock_wake_up, side_effect, state +) -> None: """Test first coordinator refresh with an error.""" - mock_wake_up.side_effect = TeslaFleetError + mock_wake_up.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state async def test_vehicle_refresh_offline( @@ -111,18 +108,24 @@ async def test_vehicle_refresh_offline( mock_vehicle_data.assert_called_once() -async def test_vehicle_refresh_error(hass: HomeAssistant, mock_vehicle_data) -> None: +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_vehicle_refresh_error( + hass: HomeAssistant, mock_vehicle_data, side_effect, state +) -> None: """Test coordinator refresh with an error.""" - mock_vehicle_data.side_effect = TeslaFleetError + mock_vehicle_data.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state # Test Energy Coordinator -async def test_energy_refresh_error(hass: HomeAssistant, mock_live_status) -> None: +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_energy_refresh_error( + hass: HomeAssistant, mock_live_status, side_effect, state +) -> None: """Test coordinator refresh with an error.""" - mock_live_status.side_effect = TeslaFleetError + mock_live_status.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index d30e6c74aef..6c355c8ddca 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -54,6 +54,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'speaker', 'friendly_name': 'Test Media player', + 'media_album_name': 'Album', + 'media_artist': 'Artist', + 'media_duration': 60.0, + 'media_playlist': 'Playlist', + 'media_position': 30.0, + 'media_title': 'Song', + 'source': 'Spotify', 'supported_features': , 'volume_level': 0.22580323309042688, }), @@ -62,6 +69,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'idle', + 'state': 'playing', }) # --- diff --git a/tests/components/tessie/test_config_flow.py b/tests/components/tessie/test_config_flow.py index e5bcf11efd1..ac3217f864b 100644 --- a/tests/components/tessie/test_config_flow.py +++ b/tests/components/tessie/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert not result1["errors"] with patch( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tessie" assert result2["data"] == TEST_CONFIG @@ -70,7 +70,7 @@ async def test_form_errors( TEST_CONFIG, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error # Complete the flow @@ -79,7 +79,7 @@ async def test_form_errors( result2["flow_id"], TEST_CONFIG, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None: @@ -100,7 +100,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No data=TEST_CONFIG, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "reauth_confirm" assert not result1["errors"] @@ -116,7 +116,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == TEST_CONFIG @@ -153,7 +153,7 @@ async def test_reauth_errors( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error # Complete the flow @@ -163,6 +163,6 @@ async def test_reauth_errors( TEST_CONFIG, ) assert "errors" not in result3 - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_entry.data == TEST_CONFIG diff --git a/tests/components/tessie/test_lock.py b/tests/components/tessie/test_lock.py index ca921583d97..0371b592f07 100644 --- a/tests/components/tessie/test_lock.py +++ b/tests/components/tessie/test_lock.py @@ -15,8 +15,9 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, Pl from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry -from .common import assert_entities, setup_platform +from .common import DOMAIN, assert_entities, setup_platform async def test_locks( @@ -24,6 +25,17 @@ async def test_locks( ) -> None: """Tests that the lock entity is correct.""" + # Create the deprecated speed limit lock entity + entity_registry.async_get_or_create( + LOCK_DOMAIN, + DOMAIN, + "VINVINVIN-vehicle_state_speed_limit_mode_active", + original_name="Charge cable lock", + has_entity_name=True, + translation_key="vehicle_state_speed_limit_mode_active", + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + ) + entry = await setup_platform(hass, [Platform.LOCK]) assert_entities(hass, entry.entry_id, entity_registry, snapshot) @@ -72,19 +84,47 @@ async def test_locks( assert hass.states.get(entity_id).state == STATE_UNLOCKED mock_run.assert_called_once() + +async def test_speed_limit_lock( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Tests that the deprecated speed limit lock entity is correct.""" + + issue_registry = async_get_issue_registry(hass) + + # Create the deprecated speed limit lock entity + entity = entity_registry.async_get_or_create( + LOCK_DOMAIN, + DOMAIN, + "VINVINVIN-vehicle_state_speed_limit_mode_active", + original_name="Charge cable lock", + has_entity_name=True, + translation_key="vehicle_state_speed_limit_mode_active", + ) + + with patch( + "homeassistant.components.tessie.lock.automations_with_entity", + return_value=["item"], + ): + await setup_platform(hass, [Platform.LOCK]) + assert issue_registry.async_get_issue( + DOMAIN, f"deprecated_speed_limit_{entity.entity_id}_item" + ) + # Test lock set value functions - entity_id = "lock.test_speed_limit" with patch( "homeassistant.components.tessie.lock.enable_speed_limit" ) as mock_enable_speed_limit: await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "1234"}, + {ATTR_ENTITY_ID: [entity.entity_id], ATTR_CODE: "1234"}, blocking=True, ) - assert hass.states.get(entity_id).state == STATE_LOCKED + assert hass.states.get(entity.entity_id).state == STATE_LOCKED mock_enable_speed_limit.assert_called_once() + # Assert issue has been raised in the issue register + assert issue_registry.async_get_issue(DOMAIN, "deprecated_speed_limit_locked") with patch( "homeassistant.components.tessie.lock.disable_speed_limit" @@ -92,16 +132,17 @@ async def test_locks( await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "1234"}, + {ATTR_ENTITY_ID: [entity.entity_id], ATTR_CODE: "1234"}, blocking=True, ) - assert hass.states.get(entity_id).state == STATE_UNLOCKED + assert hass.states.get(entity.entity_id).state == STATE_UNLOCKED mock_disable_speed_limit.assert_called_once() + assert issue_registry.async_get_issue(DOMAIN, "deprecated_speed_limit_unlocked") with pytest.raises(ServiceValidationError): await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "abc"}, + {ATTR_ENTITY_ID: [entity.entity_id], ATTR_CODE: "abc"}, blocking=True, ) diff --git a/tests/components/tessie/test_media_player.py b/tests/components/tessie/test_media_player.py index c9e4c3b84bc..008607b8018 100644 --- a/tests/components/tessie/test_media_player.py +++ b/tests/components/tessie/test_media_player.py @@ -22,6 +22,8 @@ async def test_media_player( freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_get_state, + mock_get_status, ) -> None: """Tests that the media player entity is correct when idle.""" @@ -38,6 +40,7 @@ async def test_media_player( # The refresh fixture has music playing freezer.tick(WAIT) async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_entry.entity_id) == snapshot( name=f"{entity_entry.entity_id}-playing" diff --git a/tests/components/text/common.py b/tests/components/text/common.py new file mode 100644 index 00000000000..ff989ebc26a --- /dev/null +++ b/tests/components/text/common.py @@ -0,0 +1,45 @@ +"""Common test helpers for the text entity component tests.""" + +from typing import Any + +from homeassistant.components.text import RestoreText, TextEntity + + +class MockTextEntity(TextEntity): + """Mock text class.""" + + def __init__( + self, native_value="test", native_min=None, native_max=None, pattern=None + ) -> None: + """Initialize mock text entity.""" + + self._attr_native_value = native_value + if native_min is not None: + self._attr_native_min = native_min + if native_max is not None: + self._attr_native_max = native_max + if pattern is not None: + self._attr_pattern = pattern + + def set_value(self, value: str) -> None: + """Change the selected option.""" + self._attr_native_value = value + + +class MockRestoreText(MockTextEntity, RestoreText): + """Mock RestoreText class.""" + + def __init__(self, name: str, **values: Any) -> None: + """Initialize the MockRestoreText.""" + super().__init__(**values) + + self._attr_name = name + + async def async_added_to_hass(self) -> None: + """Restore native_*.""" + await super().async_added_to_hass() + if (last_text_data := await self.async_get_last_text_data()) is None: + return + self._attr_native_max = last_text_data.native_max + self._attr_native_min = last_text_data.native_min + self._attr_native_value = last_text_data.native_value diff --git a/tests/components/text/test_device_action.py b/tests/components/text/test_device_action.py index 6a0e0958558..29e030b034e 100644 --- a/tests/components/text/test_device_action.py +++ b/tests/components/text/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.text import DOMAIN, device_action from homeassistant.const import EntityCategory diff --git a/tests/components/text/test_init.py b/tests/components/text/test_init.py index 7b44903eec3..deacf029ced 100644 --- a/tests/components/text/test_init.py +++ b/tests/components/text/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components.text import ( ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE, - TextEntity, TextMode, _async_set_value, ) @@ -24,27 +23,9 @@ from homeassistant.setup import async_setup_component from tests.common import ( async_mock_restore_state_shutdown_restart, mock_restore_cache_with_extra_data, + setup_test_component_platform, ) - - -class MockTextEntity(TextEntity): - """Mock text device to use in tests.""" - - def __init__( - self, native_value="test", native_min=None, native_max=None, pattern=None - ): - """Initialize mock text entity.""" - self._attr_native_value = native_value - if native_min is not None: - self._attr_native_min = native_min - if native_max is not None: - self._attr_native_max = native_max - if pattern is not None: - self._attr_pattern = pattern - - async def async_set_value(self, value: str) -> None: - """Set the value of the text.""" - self._attr_native_value = value +from tests.components.text.common import MockRestoreText, MockTextEntity async def test_text_default(hass: HomeAssistant) -> None: @@ -126,21 +107,16 @@ 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.text") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockRestoreText( - name="Test", - native_max=5, - native_min=1, - native_value="Hello", - ) + entity0 = MockRestoreText( + name="Test", + native_max=5, + native_min=1, + native_value="Hello", ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "text", {"text": {"platform": "test"}}) await hass.async_block_till_done() @@ -167,7 +143,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, native_min, @@ -178,18 +153,14 @@ async def test_restore_number_restore_state( """Test RestoreNumber.""" mock_restore_cache_with_extra_data(hass, ((State("text.test", ""), extra_data),)) - platform = getattr(hass.components, "test.text") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockRestoreText( - native_max=native_max, - native_min=native_min, - name="Test", - native_value=None, - ) + entity0 = MockRestoreText( + native_max=native_max, + native_min=native_min, + name="Test", + native_value=None, ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "text", {"text": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/components/thermobeacon/test_config_flow.py b/tests/components/thermobeacon/test_config_flow.py index a63ccf08963..a26a2b70c5e 100644 --- a/tests/components/thermobeacon/test_config_flow.py +++ b/tests/components/thermobeacon/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.thermobeacon.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Lanyard/mini hygrometer EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_thermobeacon(hass: HomeAssistant) -> Non context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.thermobeacon.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Lanyard/mini hygrometer EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.thermobeacon.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Lanyard/mini hygrometer EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/thermopro/test_config_flow.py b/tests/components/thermopro/test_config_flow.py index 0ee86cd5067..9b9fdd67334 100644 --- a/tests/components/thermopro/test_config_flow.py +++ b/tests/components/thermopro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.thermopro.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TP357 (2142) AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_thermopro(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_THERMOPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.thermopro.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TP357 (2142) AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.thermopro.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TP357 (2142) AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py index 9f4930947ef..c31a1937d45 100644 --- a/tests/components/thread/test_config_flow.py +++ b/tests/components/thread/test_config_flow.py @@ -42,7 +42,7 @@ async def test_import(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "import"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -65,7 +65,7 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "import"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY with patch( "homeassistant.components.thread.async_setup_entry", @@ -75,7 +75,7 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -90,7 +90,7 @@ async def test_user(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -108,7 +108,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -117,7 +117,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -144,7 +144,7 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -161,7 +161,7 @@ async def test_zeroconf_then_import(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY with patch( "homeassistant.components.thread.async_setup_entry", @@ -171,6 +171,6 @@ async def test_zeroconf_then_import(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "import"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 726fa04cef0..88c970d5c2c 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -37,7 +37,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My threshold sensor" assert result["data"] == {} assert result["options"] == { @@ -69,7 +69,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -81,7 +81,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} @@ -119,7 +119,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "hysteresis") == 0.0 @@ -133,7 +133,7 @@ async def test_options(hass: HomeAssistant) -> None: "upper": 20.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor, "hysteresis": 0.0, diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index b6c616c5cf0..28b590a29d2 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -33,7 +33,7 @@ async def test_show_config_form(recorder_mock: Recorder, hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_create_entry(recorder_mock: Recorder, hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == test_data @@ -92,7 +92,7 @@ async def test_create_entry_exceptions( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"][CONF_ACCESS_TOKEN] == expected_error @@ -109,5 +109,5 @@ async def test_flow_entry_already_exists( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 5d269bfee5d..87fe976ca3f 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest from pytile.errors import InvalidAuthError, TileError -from homeassistant import data_entry_flow from homeassistant.components.tile import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_PASSWORD, TEST_USERNAME @@ -28,7 +28,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise: @@ -38,7 +38,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -46,7 +46,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -59,7 +59,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -68,7 +68,7 @@ async def test_import_entry(hass: HomeAssistant, config, mock_pytile) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -86,12 +86,12 @@ async def test_step_reauth( assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py index b9623f9700d..fd996228034 100644 --- a/tests/components/tilt_ble/test_config_flow.py +++ b/tests/components/tilt_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.tilt_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tilt Green" assert result2["data"] == {} assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_tilt(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_TILT_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.tilt_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "F6:0F:28:F2:1F:CB"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tilt Green" assert result2["data"] == {} assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "F6:0F:28:F2:1F:CB"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.tilt_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "F6:0F:28:F2:1F:CB"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tilt Green" assert result2["data"] == {} assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" diff --git a/tests/components/time/common.py b/tests/components/time/common.py new file mode 100644 index 00000000000..f0a1c04a93f --- /dev/null +++ b/tests/components/time/common.py @@ -0,0 +1,20 @@ +"""Common helpers for time entity component tests.""" + +from datetime import time + +from homeassistant.components.time import TimeEntity + +from tests.common import MockEntity + + +class MockTimeEntity(MockEntity, TimeEntity): + """Mock time class.""" + + @property + def native_value(self) -> time | None: + """Return the current time.""" + return self._handle("native_value") + + def set_value(self, value: time) -> None: + """Change the time.""" + self._values["native_value"] = value diff --git a/tests/components/time/test_init.py b/tests/components/time/test_init.py index 20360279217..0f0dbe05e5b 100644 --- a/tests/components/time/test_init.py +++ b/tests/components/time/test_init.py @@ -2,7 +2,7 @@ from datetime import time -from homeassistant.components.time import DOMAIN, SERVICE_SET_VALUE, TimeEntity +from homeassistant.components.time import DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -12,23 +12,18 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component - -class MockTimeEntity(TimeEntity): - """Mock time device to use in tests.""" - - def __init__(self, native_value=time(12, 0, 0)) -> None: - """Initialize mock time entity.""" - self._attr_native_value = native_value - - async def async_set_value(self, value: time) -> None: - """Set the value of the time.""" - self._attr_native_value = value +from tests.common import setup_test_component_platform +from tests.components.time.common import MockTimeEntity -async def test_date(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_date(hass: HomeAssistant) -> None: """Test time entity.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockTimeEntity( + name="test", + unique_id="unique_time", + native_value=time(1, 2, 3), + ) + setup_test_component_platform(hass, DOMAIN, [entity]) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/time_date/test_config_flow.py b/tests/components/time_date/test_config_flow.py index 7402fc529d1..9f25b572014 100644 --- a/tests/components/time_date/test_config_flow.py +++ b/tests/components/time_date/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_user_flow_does_not_allow_beat( @@ -45,7 +45,7 @@ async def test_user_flow_does_not_allow_beat( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with pytest.raises(vol.Invalid): await hass.config_entries.flow.async_configure( @@ -65,13 +65,13 @@ async def test_single_instance(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"display_options": "time"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -88,7 +88,7 @@ async def test_timezone_not_set(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timezone_not_exist"} @@ -104,7 +104,7 @@ async def test_config_flow_preview( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["preview"] == "time_date" diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index c56accf103c..15c0229c653 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My tod" assert result["data"] == {} assert result["options"] == { @@ -85,7 +85,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "after_time") == "10:00" @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: "before_time": "17:05", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "after_time": "10:00", "before_time": "17:05", diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 4e52c2fff70..95024b71757 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -180,14 +180,14 @@ async def test_unload_entry( """Test unloading a config entry with a todo entity.""" config_entry = await create_mock_platform(hass, [test_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get("todo.entity1") assert state assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get("todo.entity1") assert not state diff --git a/tests/components/todoist/test_config_flow.py b/tests/components/todoist/test_config_flow.py index 141f12269de..46ae0e24fba 100644 --- a/tests/components/todoist/test_config_flow.py +++ b/tests/components/todoist/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -46,7 +46,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Todoist" assert result2.get("data") == { CONF_TOKEN: TOKEN, @@ -68,7 +68,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "invalid_api_key"} @@ -86,7 +86,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "cannot_connect"} @@ -106,7 +106,7 @@ async def test_unknown_error(hass: HomeAssistant, api: AsyncMock) -> None: }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -119,5 +119,5 @@ async def test_already_configured(hass: HomeAssistant, setup_integration: None) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/todoist/test_init.py b/tests/components/todoist/test_init.py index 62915eb0fdd..453276474b3 100644 --- a/tests/components/todoist/test_init.py +++ b/tests/components/todoist/test_init.py @@ -21,10 +21,10 @@ async def test_load_unload( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert todoist_config_entry.state == ConfigEntryState.LOADED + assert todoist_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(todoist_config_entry.entry_id) - assert todoist_config_entry.state == ConfigEntryState.NOT_LOADED + assert todoist_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize("todoist_api_status", [HTTPStatus.INTERNAL_SERVER_ERROR]) @@ -35,4 +35,4 @@ async def test_init_failure( todoist_config_entry: MockConfigEntry | None, ) -> None: """Test an initialization error on integration load.""" - assert todoist_config_entry.state == ConfigEntryState.SETUP_RETRY + assert todoist_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 711ded3880b..9dcca4b704f 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -46,7 +46,7 @@ async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock) - data={CONF_HOST: "127.0.0.1"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -59,7 +59,7 @@ async def test_user_walkthrough( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" toloclient().get_status.side_effect = lambda *args, **kwargs: None @@ -69,7 +69,7 @@ async def test_user_walkthrough( user_input={CONF_HOST: "127.0.0.2"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -80,7 +80,7 @@ async def test_user_walkthrough( user_input={CONF_HOST: "127.0.0.1"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "TOLO Sauna" assert result3["data"][CONF_HOST] == "127.0.0.1" @@ -94,7 +94,7 @@ async def test_dhcp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -102,7 +102,7 @@ async def test_dhcp( user_input={}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "TOLO Sauna" assert result["data"][CONF_HOST] == "127.0.0.2" assert result["result"].unique_id == "00:11:22:33:44:55" @@ -115,4 +115,4 @@ async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index 5d4d2e3b43b..d280e8a5182 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -9,7 +9,6 @@ from pytomorrowio.exceptions import ( UnknownException, ) -from homeassistant import data_entry_flow from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, @@ -30,6 +29,7 @@ from homeassistant.const import ( CONF_RADIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from .const import API_KEY, MIN_CONFIG @@ -42,7 +42,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_API_KEY] == API_KEY @@ -75,7 +75,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -83,7 +83,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_NAME] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_API_KEY] == API_KEY @@ -109,7 +109,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: data=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -125,7 +125,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -141,7 +141,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -157,7 +157,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "rate_limited"} @@ -173,7 +173,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -197,14 +197,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 3e8f7fa2624..7bda813e447 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -5,12 +5,12 @@ from unittest.mock import patch from toonapi import Agreement, ToonError -from homeassistant import data_entry_flow from homeassistant.components.toon.const import CONF_AGREEMENT, CONF_MIGRATE, DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -41,7 +41,7 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -58,7 +58,7 @@ async def test_full_flow_implementation( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_implementation" state = config_entry_oauth2_flow._encode_jwt( @@ -73,7 +73,7 @@ async def test_full_flow_implementation( result["flow_id"], {"implementation": "eneco"} ) - assert result2["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result2["type"] is FlowResultType.EXTERNAL_STEP assert result2["url"] == ( "https://api.toon.eu/authorize" "?response_type=code&client_id=client" @@ -149,7 +149,7 @@ async def test_no_agreements( with patch("toonapi.Toon.agreements", return_value=[]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "no_agreements" @@ -195,7 +195,7 @@ async def test_multiple_agreements( ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "agreement" result4 = await hass.config_entries.flow.async_configure( @@ -244,7 +244,7 @@ async def test_agreement_already_set_up( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -286,7 +286,7 @@ async def test_toon_abort( with patch("toonapi.Toon.agreements", side_effect=ToonError): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "connection_error" @@ -300,7 +300,7 @@ async def test_import(hass: HomeAssistant, current_request_with_host: None) -> N DOMAIN, context={"source": SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -350,7 +350,7 @@ async def test_import_migration( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr new file mode 100644 index 00000000000..8261cd74859 --- /dev/null +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -0,0 +1,117 @@ +# serializer version: 1 +# name: test_attributes[alarm_control_panel.test-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.test', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456', + 'unit_of_measurement': None, + }) +# --- +# name: test_attributes[alarm_control_panel.test-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'ac_loss': False, + 'changed_by': None, + 'code_arm_required': True, + 'code_format': None, + 'cover_tampered': False, + 'friendly_name': 'test', + 'location_id': '123456', + 'location_name': 'test', + 'low_battery': False, + 'partition': 1, + 'supported_features': , + 'triggered_source': None, + 'triggered_zone': None, + }), + 'context': , + 'entity_id': 'alarm_control_panel.test', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disarmed', + }) +# --- +# name: test_attributes[alarm_control_panel.test_partition_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.test_partition_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': 'Partition 2', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'partition', + 'unique_id': '123456_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_attributes[alarm_control_panel.test_partition_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'ac_loss': False, + 'changed_by': None, + 'code_arm_required': True, + 'code_format': None, + 'cover_tampered': False, + 'friendly_name': 'test Partition 2', + 'location_id': '123456', + 'location_name': 'test partition 2', + 'low_battery': False, + 'partition': 2, + 'supported_features': , + 'triggered_source': None, + 'triggered_zone': None, + }), + 'context': , + 'entity_id': 'alarm_control_panel.test_partition_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disarmed', + }) +# --- diff --git a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..54089c6f192 --- /dev/null +++ b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1095 @@ +# serializer version: 1 +# name: test_entity_registry[binary_sensor.fire-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.fire', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_2_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.fire-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Fire', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '2', + }), + 'context': , + 'entity_id': 'binary_sensor.fire', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.fire_battery-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': , + 'entity_id': 'binary_sensor.fire_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_2_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.fire_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Fire Battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '2', + }), + 'context': , + 'entity_id': 'binary_sensor.fire_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.fire_tamper-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': , + 'entity_id': 'binary_sensor.fire_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_2_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.fire_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Fire Tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '2', + }), + 'context': , + 'entity_id': 'binary_sensor.fire_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.gas-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.gas', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_3_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.gas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gas', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '3', + }), + 'context': , + 'entity_id': 'binary_sensor.gas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.gas_battery-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': , + 'entity_id': 'binary_sensor.gas_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_3_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.gas_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Gas Battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '3', + }), + 'context': , + 'entity_id': 'binary_sensor.gas_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.gas_tamper-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': , + 'entity_id': 'binary_sensor.gas_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_3_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.gas_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Gas Tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '3', + }), + 'context': , + 'entity_id': 'binary_sensor.gas_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.medical-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.medical', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_5_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.medical-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'safety', + 'friendly_name': 'Medical', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '5', + }), + 'context': , + 'entity_id': 'binary_sensor.medical', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.motion-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.motion', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_4_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'Motion', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '4', + }), + 'context': , + 'entity_id': 'binary_sensor.motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.motion_battery-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': , + 'entity_id': 'binary_sensor.motion_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_4_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.motion_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Motion Battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '4', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.motion_tamper-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': , + 'entity_id': 'binary_sensor.motion_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_4_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.motion_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Motion Tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '4', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.security-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.security', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_1_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.security-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Security', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '1', + }), + 'context': , + 'entity_id': 'binary_sensor.security', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.security_battery-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': , + 'entity_id': 'binary_sensor.security_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_1_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.security_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Security Battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '1', + }), + 'context': , + 'entity_id': 'binary_sensor.security_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.security_tamper-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': , + 'entity_id': 'binary_sensor.security_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_1_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.security_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Security Tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '1', + }), + 'context': , + 'entity_id': 'binary_sensor.security_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.temperature-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.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': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_7_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Temperature', + 'location_id': '123456', + 'partition': '1', + 'zone_id': 7, + }), + 'context': , + 'entity_id': 'binary_sensor.temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_battery-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': , + 'entity_id': 'binary_sensor.temperature_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_7_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Temperature Battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': 7, + }), + 'context': , + 'entity_id': 'binary_sensor.temperature_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_tamper-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': , + 'entity_id': 'binary_sensor.temperature_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_7_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Temperature Tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': 7, + }), + 'context': , + 'entity_id': 'binary_sensor.temperature_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.test_battery-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': , + 'entity_id': 'binary_sensor.test_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.test_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'test Battery', + 'location_id': '123456', + }), + 'context': , + 'entity_id': 'binary_sensor.test_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.test_power-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': , + 'entity_id': 'binary_sensor.test_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': 'Power', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.test_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'test Power', + 'location_id': '123456', + }), + 'context': , + 'entity_id': 'binary_sensor.test_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.test_tamper-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': , + 'entity_id': 'binary_sensor.test_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.test_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'test Tamper', + 'location_id': '123456', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.unknown-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.unknown', + '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': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_6_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.unknown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Unknown', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '6', + }), + 'context': , + 'entity_id': 'binary_sensor.unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_battery-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': , + 'entity_id': 'binary_sensor.unknown_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_6_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Unknown Battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '6', + }), + 'context': , + 'entity_id': 'binary_sensor.unknown_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_tamper-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': , + 'entity_id': 'binary_sensor.unknown_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_6_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Unknown Tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '6', + }), + 'context': , + 'entity_id': 'binary_sensor.unknown_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index fa2e997756d..176fe54c34a 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from total_connect_client.exceptions import ServiceUnavailable, TotalConnectError from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -14,7 +15,6 @@ from homeassistant.components.totalconnect.alarm_control_panel import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, @@ -36,7 +36,6 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt as dt_util from .common import ( - LOCATION_ID, RESPONSE_ARM_FAILURE, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY, @@ -58,7 +57,7 @@ from .common import ( setup_platform, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform ENTITY_ID = "alarm_control_panel.test" ENTITY_ID_2 = "alarm_control_panel.test_partition_2" @@ -67,28 +66,20 @@ DATA = {ATTR_ENTITY_ID: ENTITY_ID} DELAY = timedelta(seconds=10) -async def test_attributes(hass: HomeAssistant) -> None: +async def test_attributes( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test the alarm control panel attributes are correct.""" - await setup_platform(hass, ALARM_DOMAIN) + entry = await setup_platform(hass, ALARM_DOMAIN) with patch( "homeassistant.components.totalconnect.TotalConnectClient.request", return_value=RESPONSE_DISARMED, ) as mock_request: await async_update_entity(hass, ENTITY_ID) await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_ALARM_DISARMED mock_request.assert_called_once() - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" - entity_registry = er.async_get(hass) - entry = entity_registry.async_get(ENTITY_ID) - # TotalConnect partition #1 alarm device unique_id is the location_id - assert entry.unique_id == LOCATION_ID - - entry2 = entity_registry.async_get(ENTITY_ID_2) - # TotalConnect partition #2 unique_id is the location_id + "_{partition_number}" - assert entry2.unique_id == LOCATION_ID + "_2" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) assert mock_request.call_count == 1 diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index 8ff548850d9..dc433129ac8 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR, BinarySensorDeviceClass, @@ -10,41 +12,25 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import LOCATION_ID, RESPONSE_DISARMED, ZONE_NORMAL, setup_platform +from .common import RESPONSE_DISARMED, ZONE_NORMAL, setup_platform + +from tests.common import snapshot_platform ZONE_ENTITY_ID = "binary_sensor.security" -ZONE_LOW_BATTERY_ID = "binary_sensor.security_low_battery" +ZONE_LOW_BATTERY_ID = "binary_sensor.security_battery" ZONE_TAMPER_ID = "binary_sensor.security_tamper" -PANEL_BATTERY_ID = "binary_sensor.test_low_battery" +PANEL_BATTERY_ID = "binary_sensor.test_battery" PANEL_TAMPER_ID = "binary_sensor.test_tamper" PANEL_POWER_ID = "binary_sensor.test_power" -async def test_entity_registry(hass: HomeAssistant) -> None: +async def test_entity_registry( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test the binary sensor is registered in entity registry.""" - await setup_platform(hass, BINARY_SENSOR) - entity_registry = er.async_get(hass) + entry = await setup_platform(hass, BINARY_SENSOR) - # ensure zone 1 plus two diagnostic zones are created - entry = entity_registry.async_get(ZONE_ENTITY_ID) - entry_low_battery = entity_registry.async_get(ZONE_LOW_BATTERY_ID) - entry_tamper = entity_registry.async_get(ZONE_TAMPER_ID) - - assert entry.unique_id == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_zone" - assert ( - entry_low_battery.unique_id - == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_low_battery" - ) - assert entry_tamper.unique_id == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_tamper" - - # ensure panel diagnostic zones are created - panel_battery = entity_registry.async_get(PANEL_BATTERY_ID) - panel_tamper = entity_registry.async_get(PANEL_TAMPER_ID) - panel_power = entity_registry.async_get(PANEL_POWER_ID) - - assert panel_battery.unique_id == f"{LOCATION_ID}_low_battery" - assert panel_tamper.unique_id == f"{LOCATION_ID}_tamper" - assert panel_power.unique_id == f"{LOCATION_ID}_power" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_state_and_attributes(hass: HomeAssistant) -> None: @@ -63,7 +49,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: ) assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR - state = hass.states.get(f"{ZONE_ENTITY_ID}_low_battery") + state = hass.states.get(f"{ZONE_ENTITY_ID}_battery") assert state.state == STATE_OFF state = hass.states.get(f"{ZONE_ENTITY_ID}_tamper") assert state.state == STATE_OFF @@ -72,7 +58,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.fire") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.SMOKE - state = hass.states.get("binary_sensor.fire_low_battery") + state = hass.states.get("binary_sensor.fire_battery") assert state.state == STATE_ON state = hass.states.get("binary_sensor.fire_tamper") assert state.state == STATE_OFF @@ -81,7 +67,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.gas") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.GAS - state = hass.states.get("binary_sensor.gas_low_battery") + state = hass.states.get("binary_sensor.gas_battery") assert state.state == STATE_OFF state = hass.states.get("binary_sensor.gas_tamper") assert state.state == STATE_ON @@ -90,7 +76,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.unknown") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR - state = hass.states.get("binary_sensor.unknown_low_battery") + state = hass.states.get("binary_sensor.unknown_battery") assert state.state == STATE_OFF state = hass.states.get("binary_sensor.unknown_tamper") assert state.state == STATE_OFF @@ -99,7 +85,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.temperature") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.PROBLEM - state = hass.states.get("binary_sensor.temperature_low_battery") + state = hass.states.get("binary_sensor.temperature_battery") assert state.state == STATE_OFF state = hass.states.get("binary_sensor.temperature_tamper") assert state.state == STATE_OFF diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 940542bf3ad..98de748faea 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError -from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import ( AUTO_BYPASS, CONF_USERCODES, @@ -13,6 +12,7 @@ from homeassistant.components.totalconnect.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ( CONFIG_DATA, @@ -39,7 +39,7 @@ async def test_user(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -71,7 +71,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: ) # first it should show the locations form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "locations" # client should have sent four requests for init assert mock_request.call_count == 4 @@ -81,7 +81,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_USERCODES: "bad"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "locations" # client should have sent 5th request to validate usercode assert mock_request.call_count == 5 @@ -91,7 +91,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: result2["flow_id"], user_input={CONF_USERCODES: "7890"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY # client should have sent another request to validate usercode assert mock_request.call_count == 6 @@ -112,7 +112,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -128,7 +128,7 @@ async def test_login_failed(hass: HomeAssistant) -> None: data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -144,7 +144,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -161,7 +161,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -171,7 +171,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -205,7 +205,7 @@ async def test_no_locations(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONFIG_DATA_NO_USERCODES, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_locations" await hass.async_block_till_done() @@ -236,14 +236,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={AUTO_BYPASS: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {AUTO_BYPASS: True} await hass.async_block_till_done() diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 4c1cc999f16..7bf3b8cce5e 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.tplink import ( SmartDeviceException, ) from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -55,13 +56,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -69,13 +70,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -92,7 +93,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == CREATE_ENTRY_DATA_LEGACY mock_setup.assert_called_once() @@ -102,7 +103,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -110,7 +111,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -132,7 +133,7 @@ async def test_discovery_auth( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -184,7 +185,7 @@ async def test_discovery_auth_errors( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -235,7 +236,7 @@ async def test_discovery_new_credentials( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -287,7 +288,7 @@ async def test_discovery_new_credentials_invalid( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -661,7 +662,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -770,7 +771,7 @@ async def test_integration_discovery_with_ip_change( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 @@ -803,7 +804,7 @@ async def test_integration_discovery_with_ip_change( mock_connect["connect"].return_value = bulb await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED # Check that init set the new host correctly before calling connect assert config.host == "127.0.0.1" config.host = "127.0.0.2" @@ -822,7 +823,7 @@ async def test_dhcp_discovery_with_ip_change( with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 @@ -851,7 +852,7 @@ async def test_reauth( mock_added_config_entry.async_start_reauth(hass) await hass.async_block_till_done() - assert mock_added_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_added_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 [result] = flows @@ -888,7 +889,7 @@ async def test_reauth_update_from_discovery( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -924,7 +925,7 @@ async def test_reauth_update_from_discovery_with_ip_change( with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -967,7 +968,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same( ) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -1013,7 +1014,7 @@ async def test_reauth_errors( mock_added_config_entry.async_start_reauth(hass) await hass.async_block_till_done() - assert mock_added_config_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_added_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 [result] = flows @@ -1104,7 +1105,7 @@ async def test_pick_device_errors( CONF_PASSWORD: "fake_password", }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["context"]["unique_id"] == MAC_ADDRESS @@ -1155,8 +1156,8 @@ async def test_reauth_update_other_flows( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry2.state == config_entries.ConfigEntryState.SETUP_ERROR - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry2.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR mock_connect["connect"].side_effect = default_side_effect await hass.async_block_till_done() diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 176f2aab7ae..b8f623ac6dc 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -77,10 +77,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_single_discovery(), _patch_connect(): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.LOADED + assert already_migrated_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + assert already_migrated_config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry(hass: HomeAssistant) -> None: @@ -96,7 +96,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: ): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( @@ -154,7 +154,7 @@ async def test_config_entry_wrong_mac_Address( with _patch_discovery(), _patch_single_discovery(), _patch_connect(): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY assert ( "Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:f0, found aa:bb:cc:dd:ee:ff" diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 767ff4a122c..1217a4d4cca 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, PropertyMock +from kasa import AuthenticationException, SmartDeviceException, TimeoutException import pytest from homeassistant.components import tplink @@ -24,8 +25,10 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.tplink.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON 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 @@ -730,3 +733,66 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: } ) strip.set_custom_effect.reset_mock() + + +@pytest.mark.parametrize( + ("exception_type", "msg", "reauth_expected"), + [ + ( + AuthenticationException, + "Device authentication error async_turn_on: test error", + True, + ), + ( + TimeoutException, + "Timeout communicating with the device async_turn_on: test error", + False, + ), + ( + SmartDeviceException, + "Unable to communicate with the device async_turn_on: test error", + False, + ), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_light_errors_when_turned_on( + hass: HomeAssistant, + exception_type, + msg, + reauth_expected, +) -> None: + """Tests the light wraps errors correctly.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.turn_on.side_effect = exception_type(msg) + + with _patch_discovery(device=bulb), _patch_connect(device=bulb): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + assert not any( + already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + ) + + with pytest.raises(HomeAssistantError, match=msg): + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert bulb.turn_on.call_count == 1 + assert ( + any( + flow + for flow in already_migrated_config_entry.async_get_active_flows( + hass, {SOURCE_REAUTH} + ) + if flow["handler"] == tplink.DOMAIN + ) + == reauth_expected + ) diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 6326e9bb671..6fb841346a1 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -3,12 +3,13 @@ from datetime import timedelta from unittest.mock import AsyncMock -from kasa import SmartDeviceException +from kasa import AuthenticationException, SmartDeviceException, TimeoutException 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.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -17,6 +18,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) 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 homeassistant.util import dt as dt_util, slugify @@ -202,3 +204,66 @@ async def test_strip_unique_ids(hass: HomeAssistant) -> None: assert ( entity_registry.async_get(entity_id).unique_id == f"PLUG{plug_id}DEVICEID" ) + + +@pytest.mark.parametrize( + ("exception_type", "msg", "reauth_expected"), + [ + ( + AuthenticationException, + "Device authentication error async_turn_on: test error", + True, + ), + ( + TimeoutException, + "Timeout communicating with the device async_turn_on: test error", + False, + ), + ( + SmartDeviceException, + "Unable to communicate with the device async_turn_on: test error", + False, + ), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_plug_errors_when_turned_on( + hass: HomeAssistant, + exception_type, + msg, + reauth_expected, +) -> None: + """Tests the plug wraps errors correctly.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_plug() + plug.turn_on.side_effect = exception_type("test error") + + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.my_plug" + + assert not any( + already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + ) + + with pytest.raises(HomeAssistantError, match=msg): + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert plug.turn_on.call_count == 1 + assert ( + any( + flow + for flow in already_migrated_config_entry.async_get_active_flows( + hass, {SOURCE_REAUTH} + ) + if flow["handler"] == tplink.DOMAIN + ) + == reauth_expected + ) diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py index 230f0d2a68e..08606fe126c 100644 --- a/tests/components/tplink_omada/test_config_flow.py +++ b/tests/components/tplink_omada/test_config_flow.py @@ -43,7 +43,7 @@ async def test_form_single_site(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -64,7 +64,7 @@ async def test_form_single_site(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "OC200 (Display Name)" assert result2["data"] == MOCK_ENTRY_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -76,7 +76,7 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -100,7 +100,7 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "site" with patch( @@ -115,7 +115,7 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "OC200 (Site 2)" assert result3["data"] == { "host": "https://fake.omada.host", @@ -142,7 +142,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -161,7 +161,7 @@ async def test_form_api_error(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -180,7 +180,7 @@ async def test_form_generic_exception(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -199,7 +199,7 @@ async def test_form_unsupported_controller(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unsupported_controller"} @@ -218,7 +218,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -237,7 +237,7 @@ async def test_form_no_sites(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_sites_found"} @@ -260,7 +260,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -274,7 +274,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" mocked_validate.assert_called_once_with( hass, @@ -307,7 +307,7 @@ async def test_async_step_reauth_invalid_auth(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -319,7 +319,7 @@ async def test_async_step_reauth_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/tplink_omada/test_switch.py b/tests/components/tplink_omada/test_switch.py index 78b22a4e829..be2c21d02ab 100644 --- a/tests/components/tplink_omada/test_switch.py +++ b/tests/components/tplink_omada/test_switch.py @@ -160,7 +160,7 @@ async def test_gateway_port_poe_switch( assert entity.state == "on" -async def test_gaateway_wan_port_has_no_poe_switch( +async def test_gateway_wan_port_has_no_poe_switch( hass: HomeAssistant, init_integration: MockConfigEntry, ) -> None: diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index e0ce876a97f..79e5c877563 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -5,13 +5,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import traccar, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_HOME, STATE_NOT_HOME 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 homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -65,10 +66,10 @@ async def webhook_id_fixture(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/traccar_server/fixtures/devices.json b/tests/components/traccar_server/fixtures/devices.json index b04d53d9fdf..f3db1322e0b 100644 --- a/tests/components/traccar_server/fixtures/devices.json +++ b/tests/components/traccar_server/fixtures/devices.json @@ -3,7 +3,7 @@ "id": 0, "name": "X-Wing", "uniqueId": "abc123", - "status": "unknown", + "status": "online", "disabled": false, "lastUpdate": "1970-01-01T00:00:00Z", "positionId": 0, diff --git a/tests/components/traccar_server/fixtures/positions.json b/tests/components/traccar_server/fixtures/positions.json index 6b65116e804..7f661a7092a 100644 --- a/tests/components/traccar_server/fixtures/positions.json +++ b/tests/components/traccar_server/fixtures/positions.json @@ -18,7 +18,8 @@ "network": {}, "geofenceIds": [0], "attributes": { - "custom_attr_1": "custom_attr_1_value" + "custom_attr_1": "custom_attr_1_value", + "batteryLevel": 15.00000867601 } } ] diff --git a/tests/components/traccar_server/fixtures/server.json b/tests/components/traccar_server/fixtures/server.json index 039b6bfa1f4..7de1152b63d 100644 --- a/tests/components/traccar_server/fixtures/server.json +++ b/tests/components/traccar_server/fixtures/server.json @@ -17,5 +17,7 @@ "coordinateFormat": null, "openIdEnabled": true, "openIdForce": true, - "attributes": {} + "attributes": { + "speedUnit": "kn" + } } diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index f8fe3cc60f7..39e67db8df7 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -30,7 +30,7 @@ 'name': 'X-Wing', 'phone': None, 'positionId': 0, - 'status': 'unknown', + 'status': 'online', 'uniqueId': 'abc123', }), 'geofence': dict({ @@ -47,6 +47,7 @@ 'address': '**REDACTED**', 'altitude': 546841384638, 'attributes': dict({ + 'batteryLevel': 15.00000867601, 'custom_attr_1': 'custom_attr_1_value', }), 'course': 360, @@ -72,28 +73,108 @@ 'entities': list([ dict({ 'disabled': False, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'binary_sensor.x_wing_motion', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'X-Wing Motion', + }), + 'state': 'off', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'binary_sensor.x_wing_status', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Status', + }), + 'state': 'on', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ - 'address': '**REDACTED**', - 'altitude': 546841384638, - 'battery_level': -1, 'category': 'starfighter', 'custom_attr_1': 'custom_attr_1_value', 'friendly_name': 'X-Wing', - 'geofence': 'Tatooine', 'gps_accuracy': 3.5, 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', - 'motion': False, 'source_type': 'gps', - 'speed': 4568795, - 'status': 'unknown', 'traccar_id': 0, 'tracker': 'traccar_server', }), 'state': 'not_home', }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_address', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Address', + }), + 'state': '**REDACTED**', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_altitude', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Altitude', + 'state_class': 'measurement', + 'unit_of_measurement': 'm', + }), + 'state': '546841384638', + }), + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_battery', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'X-Wing Battery', + 'state_class': 'measurement', + 'unit_of_measurement': '%', + }), + 'state': '15.00000867601', + }), + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_geofence', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Geofence', + }), + 'state': 'Tatooine', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_speed', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'speed', + 'friendly_name': 'X-Wing Speed', + 'state_class': 'measurement', + 'unit_of_measurement': 'kn', + }), + 'state': '4568795', + }), + 'unit_of_measurement': 'kn', }), ]), 'subscription_status': 'disconnected', @@ -130,7 +211,7 @@ 'name': 'X-Wing', 'phone': None, 'positionId': 0, - 'status': 'unknown', + 'status': 'online', 'uniqueId': 'abc123', }), 'geofence': dict({ @@ -147,6 +228,7 @@ 'address': '**REDACTED**', 'altitude': 546841384638, 'attributes': dict({ + 'batteryLevel': 15.00000867601, 'custom_attr_1': 'custom_attr_1_value', }), 'course': 360, @@ -172,8 +254,51 @@ 'entities': list([ dict({ 'disabled': True, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'binary_sensor.x_wing_motion', 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'binary_sensor.x_wing_status', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'device_tracker.x_wing', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_address', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_altitude', + 'state': None, + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_geofence', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_speed', + 'state': None, + 'unit_of_measurement': 'kn', }), ]), 'subscription_status': 'disconnected', @@ -210,7 +335,7 @@ 'name': 'X-Wing', 'phone': None, 'positionId': 0, - 'status': 'unknown', + 'status': 'online', 'uniqueId': 'abc123', }), 'geofence': dict({ @@ -227,6 +352,7 @@ 'address': '**REDACTED**', 'altitude': 546841384638, 'attributes': dict({ + 'batteryLevel': 15.00000867601, 'custom_attr_1': 'custom_attr_1_value', }), 'course': 360, @@ -250,30 +376,66 @@ }), }), 'entities': list([ + dict({ + 'disabled': True, + 'entity_id': 'binary_sensor.x_wing_motion', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'binary_sensor.x_wing_status', + 'state': None, + 'unit_of_measurement': None, + }), dict({ 'disabled': False, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ - 'address': '**REDACTED**', - 'altitude': 546841384638, - 'battery_level': -1, 'category': 'starfighter', 'custom_attr_1': 'custom_attr_1_value', 'friendly_name': 'X-Wing', - 'geofence': 'Tatooine', 'gps_accuracy': 3.5, 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', - 'motion': False, 'source_type': 'gps', - 'speed': 4568795, - 'status': 'unknown', 'traccar_id': 0, 'tracker': 'traccar_server', }), 'state': 'not_home', }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_address', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_altitude', + 'state': None, + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_geofence', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_speed', + 'state': None, + 'unit_of_measurement': 'kn', }), ]), 'subscription_status': 'disconnected', diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index c412830066d..fdc22f9ff97 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.traccar_server.const import ( DOMAIN, EVENTS, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +40,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -52,7 +53,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" assert result["data"] == { CONF_HOST: "1.1.1.1", @@ -62,7 +63,7 @@ async def test_form( CONF_SSL: False, CONF_VERIFY_SSL: True, } - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -94,7 +95,7 @@ async def test_form_cannot_connect( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} mock_traccar_api_client.get_server.side_effect = None @@ -109,7 +110,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" assert result["data"] == { CONF_HOST: "1.1.1.1", @@ -120,7 +121,7 @@ async def test_form_cannot_connect( CONF_VERIFY_SSL: True, } - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED async def test_options( @@ -143,7 +144,7 @@ async def test_options( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == { CONF_MAX_ACCURACY: 2.0, CONF_EVENTS: [], @@ -238,11 +239,11 @@ async def test_import_from_yaml( context={"source": config_entries.SOURCE_IMPORT}, data=PLATFORM_SCHEMA({"platform": "traccar", **imported}), ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{data[CONF_HOST]}:{data[CONF_PORT]}" assert result["data"] == data assert result["options"] == options - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED async def test_abort_import_already_configured(hass: HomeAssistant) -> None: @@ -269,7 +270,7 @@ async def test_abort_import_already_configured(hass: HomeAssistant) -> None: ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -296,5 +297,5 @@ async def test_abort_already_configured( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 3d112057315..9019cd0ebf1 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -33,6 +33,10 @@ async def test_entry_diagnostics( hass_client, mock_config_entry, ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name="entry") @@ -44,23 +48,37 @@ async def test_device_diagnostics( mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test device diagnostics.""" await setup_integration(hass, mock_config_entry) devices = dr.async_entries_for_config_entry( - hass.helpers.device_registry.async_get(hass), + device_registry, 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 + device_registry, mock_config_entry.entry_id ): + entities = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + # Enable all entities to show everything in snapshots + for entity in entities: + entity_registry.async_update_entity(entity.entity_id, disabled_by=None) + result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name=device.name) @@ -78,14 +96,14 @@ async def 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), + device_registry, 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 + device_registry, mock_config_entry.entry_id ): for entry in er.async_entries_for_device( entity_registry, @@ -100,5 +118,9 @@ async def test_device_diagnostics_with_disabled_entity( result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) 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 5c5d882b721..f2cfb6a109f 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -133,12 +133,12 @@ async def test_get_trace( ) -> None: """Test tracing a script or automation.""" await async_setup_component(hass, "homeassistant", {}) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -429,12 +429,12 @@ async def test_restore_traces( ) -> None: """Test restored traces.""" hass.set_state(CoreState.not_running) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) hass_storage["trace.saved_traces"] = saved_traces @@ -522,7 +522,7 @@ async def test_trace_overflow( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, stored_traces ) -> None: """Test the number of stored traces per script or automation is limited.""" - id = 1 + msg_id = 1 trace_uuids = [] @@ -532,9 +532,9 @@ async def test_trace_overflow( return trace_uuids[-1] def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -601,7 +601,7 @@ async def test_restore_traces_overflow( ) -> None: """Test restored traces are evicted first.""" hass.set_state(CoreState.not_running) - id = 1 + msg_id = 1 trace_uuids = [] @@ -611,9 +611,9 @@ async def test_restore_traces_overflow( return trace_uuids[-1] def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) hass_storage["trace.saved_traces"] = saved_traces @@ -682,7 +682,7 @@ async def test_restore_traces_late_overflow( ) -> None: """Test restored traces are evicted first.""" hass.set_state(CoreState.not_running) - id = 1 + msg_id = 1 trace_uuids = [] @@ -692,9 +692,9 @@ async def test_restore_traces_late_overflow( return trace_uuids[-1] def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) hass_storage["trace.saved_traces"] = saved_traces @@ -743,12 +743,12 @@ async def test_trace_no_traces( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain ) -> None: """Test the storing traces for a script or automation can be disabled.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -810,12 +810,12 @@ async def test_list_traces( ) -> None: """Test listing script and automation traces.""" await async_setup_component(hass, "homeassistant", {}) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -943,12 +943,12 @@ async def test_nested_traces( extra_trace_keys, ) -> None: """Test nested automation and script traces.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -1003,12 +1003,12 @@ async def test_breakpoints( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, prefix ) -> None: """Test script and automation breakpoints.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( @@ -1173,12 +1173,12 @@ async def test_breakpoints_2( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, prefix ) -> None: """Test execution resumes and breakpoints are removed after subscription removed.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( @@ -1278,12 +1278,12 @@ async def test_breakpoints_3( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, prefix ) -> None: """Test breakpoints can be cleared.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( @@ -1434,12 +1434,12 @@ async def test_script_mode( script_execution, ) -> None: """Test overlapping runs with max_runs > 1.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id flag = asyncio.Event() @@ -1502,12 +1502,12 @@ async def test_script_mode_2( script_execution, ) -> None: """Test overlapping runs with max_runs > 1.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id flag = asyncio.Event() @@ -1577,12 +1577,12 @@ async def test_trace_blueprint_automation( ) -> None: """Test trace of blueprint automation.""" await async_setup_component(hass, "homeassistant", {}) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id domain = "automation" sun_config = { diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 45a37730ff4..5cedb51e5af 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -7,6 +7,7 @@ import aiotractive from homeassistant import config_entries from homeassistant.components.tractive.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -38,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-email@example.com" assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +60,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -78,7 +79,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -96,7 +97,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -119,7 +120,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -136,7 +137,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -160,7 +161,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -175,7 +176,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "invalid_auth" @@ -198,7 +199,7 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -213,7 +214,7 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -236,7 +237,7 @@ async def test_reauthentication_failure_no_existing_entry(hass: HomeAssistant) - data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -247,5 +248,5 @@ async def test_reauthentication_failure_no_existing_entry(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index fd3d85461b1..af2fdc22d2a 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.tradfri import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TRADFRI_PATH @@ -38,7 +39,7 @@ async def test_already_paired(hass: HomeAssistant, mock_entry_setup) -> None: result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_authenticate"} @@ -58,7 +59,7 @@ async def test_user_connection_successful( assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "host": "123.123.123.123", "gateway_id": "bla", @@ -81,7 +82,7 @@ async def test_user_connection_timeout( assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout"} @@ -101,7 +102,7 @@ async def test_user_connection_bad_key( assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"security_code": "invalid_security_code"} @@ -131,7 +132,7 @@ async def test_discovery_connection( assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "homekit-id" assert result["result"].data == { "host": "123.123.123.123", @@ -160,7 +161,7 @@ async def test_discovery_duplicate_aborted(hass: HomeAssistant) -> None: ), ) - assert flow["type"] == data_entry_flow.FlowResultType.ABORT + assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.data["host"] == "123.123.123.124" @@ -184,7 +185,7 @@ async def test_duplicate_discovery( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( "tradfri", @@ -200,7 +201,7 @@ async def test_duplicate_discovery( ), ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: @@ -225,7 +226,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: ), ) - assert flow["type"] == data_entry_flow.FlowResultType.ABORT + assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.unique_id == "homekit-id" diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index eb14636d6c9..8162db076fa 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test Camera" assert result2["data"] == { "api_key": "1234567890", @@ -63,7 +63,7 @@ async def test_form_multiple_cameras( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -97,7 +97,7 @@ async def test_form_multiple_cameras( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Camera2" assert result["data"] == { "api_key": "1234567890", @@ -115,7 +115,7 @@ async def test_form_no_location_data( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -137,7 +137,7 @@ async def test_form_no_location_data( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test Camera" assert result2["data"] == { "api_key": "1234567890", @@ -175,7 +175,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -216,7 +216,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -234,7 +234,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -299,7 +299,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {error_key: p_error} with ( @@ -317,7 +317,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py index 4f633cb524d..3f37ad05575 100644 --- a/tests/components/trafikverket_camera/test_coordinator.py +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -12,10 +12,9 @@ from pytrafikverket.exceptions import ( UnknownError, ) -from homeassistant import config_entries from homeassistant.components.trafikverket_camera.const import DOMAIN from homeassistant.components.trafikverket_camera.coordinator import CameraData -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import UpdateFailed @@ -65,22 +64,22 @@ async def test_coordinator( ( InvalidAuthentication, ConfigEntryAuthFailed, - config_entries.ConfigEntryState.SETUP_ERROR, + ConfigEntryState.SETUP_ERROR, ), ( NoCameraFound, UpdateFailed, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_RETRY, ), ( MultipleCamerasFound, UpdateFailed, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_RETRY, ), ( UnknownError, UpdateFailed, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_RETRY, ), ], ) @@ -152,4 +151,4 @@ async def test_coordinator_failed_get_image( mock_data.assert_called_once() state = hass.states.get("camera.test_camera") assert state is None - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py index 688af08fec1..f21d36fda27 100644 --- a/tests/components/trafikverket_camera/test_init.py +++ b/tests/components/trafikverket_camera/test_init.py @@ -9,10 +9,9 @@ import pytest from pytrafikverket.exceptions import UnknownError from pytrafikverket.trafikverket_camera import CameraInfo -from homeassistant import config_entries from homeassistant.components.trafikverket_camera import async_migrate_entry from homeassistant.components.trafikverket_camera.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -50,7 +49,7 @@ async def test_setup_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(mock_tvt_camera.mock_calls) == 1 @@ -82,10 +81,10 @@ async def test_unload_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_migrate_entry( @@ -115,7 +114,7 @@ async def test_migrate_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.version == 3 assert entry.unique_id == "trafikverket_camera-1234" assert entry.data == ENTRY_CONFIG @@ -165,7 +164,7 @@ async def test_migrate_entry_fails_with_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == version assert entry.unique_id == unique_id assert len(mock_tvt_camera.mock_calls) == 1 @@ -229,7 +228,7 @@ async def test_migrate_entry_fails_no_id( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == version assert entry.unique_id == unique_id assert len(mock_tvt_camera.mock_calls) == 1 diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index 2a0a0ae6cd6..1c170a917cc 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -51,7 +51,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Ekerö to Slagsta at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -92,7 +92,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -138,7 +138,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -156,7 +156,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -224,7 +224,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -242,7 +242,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_ferry/test_init.py b/tests/components/trafikverket_ferry/test_init.py index adfc84d94cb..22ada7e0f40 100644 --- a/tests/components/trafikverket_ferry/test_init.py +++ b/tests/components/trafikverket_ferry/test_init.py @@ -6,9 +6,8 @@ from unittest.mock import patch from pytrafikverket.trafikverket_ferry import FerryStop -from homeassistant import config_entries from homeassistant.components.trafikverket_ferry.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from . import ENTRY_CONFIG @@ -34,7 +33,7 @@ async def test_setup_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(mock_tvt_ferry.mock_calls) == 1 @@ -56,7 +55,7 @@ async def test_unload_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 3a5afa7431c..a6ba82a85bc 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -61,7 +61,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Stockholm C to Uppsala C at 10:00" assert result["data"] == { "api_key": "1234567890", @@ -98,7 +98,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -125,7 +125,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -158,7 +158,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -203,7 +203,7 @@ async def test_flow_fails_departures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -256,7 +256,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -277,7 +277,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -354,7 +354,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": p_error} with ( @@ -375,7 +375,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -444,7 +444,7 @@ async def test_reauth_flow_error_departures( await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": p_error} with ( @@ -465,7 +465,7 @@ async def test_reauth_flow_error_departures( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -515,7 +515,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -524,12 +524,12 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"filter_product": "SJ Regionaltåg"} result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -538,5 +538,5 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"filter_product": None} diff --git a/tests/components/trafikverket_train/test_init.py b/tests/components/trafikverket_train/test_init.py index f68c32b5b90..329d8d716d0 100644 --- a/tests/components/trafikverket_train/test_init.py +++ b/tests/components/trafikverket_train/test_init.py @@ -8,9 +8,8 @@ from pytrafikverket.exceptions import InvalidAuthentication, NoTrainStationFound from pytrafikverket.trafikverket_train import TrainStop from syrupy.assertion import SnapshotAssertion -from homeassistant import config_entries from homeassistant.components.trafikverket_train.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -43,12 +42,12 @@ async def test_unload_entry(hass: HomeAssistant, get_trains: list[TrainStop]) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(mock_tv_train.mock_calls) == 1 assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_auth_failed( @@ -74,7 +73,7 @@ async def test_auth_failed( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR active_flows = entry.async_get_active_flows(hass, (SOURCE_REAUTH)) for flow in active_flows: @@ -104,7 +103,7 @@ async def test_no_stations( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_migrate_entity_unique_id( @@ -144,7 +143,7 @@ async def test_migrate_entity_unique_id( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED entity = entity_registry.async_get(entity.entity_id) assert entity.unique_id == f"{entry.entry_id}-departure_time" diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index 4a1c50cbaf1..771336301ff 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -49,7 +49,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Vallby" assert result2["data"] == { "api_key": "1234567890", @@ -87,7 +87,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -125,7 +125,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -143,7 +143,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891", "station": "Vallby"} @@ -191,7 +191,7 @@ async def test_reauth_flow_fails( data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -204,5 +204,5 @@ async def test_reauth_flow_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 0e184ffc96b..e6c523bf1f6 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.transmission.async_setup_entry", @@ -44,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Transmission" assert result2["data"] == MOCK_CONFIG_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +60,7 @@ async def test_device_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -68,7 +68,7 @@ async def test_device_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -90,14 +90,14 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"limit": 20} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["limit"] == 20 assert result["data"]["order"] == "oldest_first" @@ -115,7 +115,7 @@ async def test_error_on_wrong_credentials( result["flow_id"], MOCK_CONFIG_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "username": "invalid_auth", "password": "invalid_auth", @@ -133,7 +133,7 @@ async def test_unexpected_error(hass: HomeAssistant, mock_api: MagicMock) -> Non result["flow_id"], MOCK_CONFIG_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -151,7 +151,7 @@ async def test_error_on_connection_failure( MOCK_CONFIG_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -169,7 +169,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_CONFIG_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} @@ -184,7 +184,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -206,7 +206,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: data=MOCK_CONFIG_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} @@ -218,7 +218,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -241,7 +241,7 @@ async def test_reauth_failed_connection_error( data=MOCK_CONFIG_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} @@ -253,5 +253,5 @@ async def test_reauth_failed_connection_error( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 7efbaad76fb..307576ffdea 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -41,7 +41,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_config_flow_entry_migrate_1_1_to_1_2(hass: HomeAssistant) -> None: @@ -76,7 +76,7 @@ async def test_setup_failed_connection_error( mock_api.side_effect = TransmissionConnectError() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_failed_auth_error( @@ -90,7 +90,7 @@ async def test_setup_failed_auth_error( mock_api.side_effect = TransmissionAuthError() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_failed_unexpected_error( @@ -104,7 +104,7 @@ async def test_setup_failed_unexpected_error( mock_api.side_effect = TransmissionError() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass: HomeAssistant) -> None: diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index 9ecff818592..80df947dbe7 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -19,7 +19,7 @@ VALID_CONFIG = { def get_departuresMock(_stop_id, route, destination, api_key): """Mock TransportNSW departures loading.""" - data = { + return { "stop_id": "209516", "route": "199", "due": 16, @@ -28,7 +28,6 @@ def get_departuresMock(_stop_id, route, destination, api_key): "destination": "Palm Beach", "mode": "Bus", } - return data @patch("TransportNSW.TransportNSW.get_departures", side_effect=get_departuresMock) @@ -50,7 +49,7 @@ async def test_transportnsw_config(mocked_get_departures, hass: HomeAssistant) - def get_departuresMock_notFound(_stop_id, route, destination, api_key): """Mock TransportNSW departures loading.""" - data = { + return { "stop_id": "n/a", "route": "n/a", "due": "n/a", @@ -59,7 +58,6 @@ def get_departuresMock_notFound(_stop_id, route, destination, api_key): "destination": "n/a", "mode": "n/a", } - return data @patch( diff --git a/tests/components/trend/test_config_flow.py b/tests/components/trend/test_config_flow.py index baccc396bf1..fb82589b3ce 100644 --- a/tests/components/trend/test_config_flow.py +++ b/tests/components/trend/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM # test step 2 of config flow: settings of trend sensor with patch( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "CPU Temperature rising" assert result["data"] == {} assert result["options"] == { @@ -57,7 +57,7 @@ async def test_options(hass: HomeAssistant, config_entry: MockConfigEntry) -> No config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -69,7 +69,7 @@ async def test_options(hass: HomeAssistant, config_entry: MockConfigEntry) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "min_samples": 30, "max_samples": 50, diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 2c58c25a509..7d308ec0b23 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -82,7 +82,7 @@ async def test_config_entry_unload( assert state is None config_entry = await mock_config_entry_setup(hass, mock_tts_entity) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNKNOWN @@ -115,7 +115,7 @@ async def test_config_entry_unload( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert state is None @@ -133,7 +133,7 @@ async def test_restore_state( config_entry = await mock_config_entry_setup(hass, mock_tts_entity) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert state.state == timestamp diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 646d6a09f12..6e971262bc8 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_flow( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -36,7 +36,7 @@ async def test_user_flow( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "scan" result3 = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_user_flow( user_input={}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3 == snapshot @@ -58,7 +58,7 @@ async def test_user_flow_failed_qr_code( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Something went wrong getting the QR code (like an invalid user code) @@ -69,7 +69,7 @@ async def test_user_flow_failed_qr_code( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "login_error"} # This time it worked out @@ -86,7 +86,7 @@ async def test_user_flow_failed_qr_code( user_input={}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY async def test_user_flow_failed_scan( @@ -99,7 +99,7 @@ async def test_user_flow_failed_scan( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -107,7 +107,7 @@ async def test_user_flow_failed_scan( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "scan" # Access has been denied, or the code hasn't been scanned yet @@ -122,7 +122,7 @@ async def test_user_flow_failed_scan( user_input={}, ) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("errors") == {"base": "login_error"} # This time it worked out @@ -133,7 +133,7 @@ async def test_user_flow_failed_scan( user_input={}, ) - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("mock_tuya_login_control") @@ -155,7 +155,7 @@ async def test_reauth_flow( data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "scan" result2 = await hass.config_entries.flow.async_configure( @@ -163,7 +163,7 @@ async def test_reauth_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry == snapshot @@ -195,7 +195,7 @@ async def test_reauth_flow_migration( data=mock_old_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_user_code" result2 = await hass.config_entries.flow.async_configure( @@ -203,7 +203,7 @@ async def test_reauth_flow_migration( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "scan" result3 = await hass.config_entries.flow.async_configure( @@ -211,7 +211,7 @@ async def test_reauth_flow_migration( user_input={}, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" # Ensure the old data is gone, new data is present @@ -247,7 +247,7 @@ async def test_reauth_flow_failed_qr_code( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "login_error"} # This time it worked out @@ -264,5 +264,5 @@ async def test_reauth_flow_failed_qr_code( user_input={}, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index e272ce38bee..dbc01c69acb 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -42,7 +42,7 @@ async def test_full_user_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -60,7 +60,7 @@ async def test_invalid_address( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_twentemilieu.unique_id.side_effect = TwenteMilieuAddressError @@ -72,7 +72,7 @@ async def test_invalid_address( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "invalid_address"} @@ -85,7 +85,7 @@ async def test_invalid_address( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3 == snapshot @@ -106,7 +106,7 @@ async def test_connection_error( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -128,5 +128,5 @@ async def test_address_already_set_up( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 77e6afe3a12..8efa1c24742 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,9 +1,10 @@ """Test the init file of Twilio.""" -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import twilio from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from tests.typing import ClientSessionGenerator @@ -19,10 +20,10 @@ async def test_config_flow_registers_webhook( result = await hass.config_entries.flow.async_init( "twilio", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] twilio_events = [] diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index f797f9b01b6..9b9aeafd082 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant.components import dhcp from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_MODEL, TEST_NAME, ClientMock @@ -23,7 +24,7 @@ async def test_invalid_host(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -31,7 +32,7 @@ async def test_invalid_host(hass: HomeAssistant) -> None: {CONF_HOST: "dummy"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -49,7 +50,7 @@ async def test_success_flow(hass: HomeAssistant) -> None: TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -58,7 +59,7 @@ async def test_success_flow(hass: HomeAssistant) -> None: {CONF_HOST: "dummy"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: "dummy", @@ -85,7 +86,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" @@ -109,12 +110,12 @@ async def test_dhcp_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -154,5 +155,5 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index 33f24a31d8f..6642807ac3f 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -17,16 +17,16 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: """Validate that setup entry also configure the client.""" client = ClientMock() - id = str(uuid4()) + device_id = str(uuid4()) config_entry = MockConfigEntry( domain=TWINKLY_DOMAIN, data={ CONF_HOST: TEST_HOST, - CONF_ID: id, + CONF_ID: device_id, CONF_NAME: TEST_NAME_ORIGINAL, CONF_MODEL: TEST_MODEL, }, - entry_id=id, + entry_id=device_id, ) config_entry.add_to_hass(hass) @@ -34,11 +34,11 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: with patch("homeassistant.components.twinkly.Twinkly", return_value=client): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_not_ready(hass: HomeAssistant) -> None: diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index 4b6834ba544..94fa2ce0427 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -2,23 +2,20 @@ from unittest.mock import patch -import pytest - from homeassistant.components.twitch.const import ( CONF_CHANNELS, DOMAIN, OAUTH2_AUTHORIZE, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType -from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir +from homeassistant.helpers import config_entry_oauth2_flow from . import setup_integration from tests.common import MockConfigEntry -from tests.components.twitch import TwitchInvalidTokenMock, TwitchMock +from tests.components.twitch import TwitchMock from tests.components.twitch.conftest import CLIENT_ID, TITLE from tests.typing import ClientSessionGenerator @@ -67,7 +64,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "channel123" assert "result" in result assert "token" in result["result"].data @@ -98,7 +95,7 @@ async def test_already_configured( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -121,7 +118,7 @@ async def test_reauth( }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -130,7 +127,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -195,7 +192,7 @@ async def test_reauth_wrong_account( }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -204,93 +201,5 @@ async def test_reauth_wrong_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" - - -async def test_import( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, - mock_setup_entry, - twitch: TwitchMock, -) -> None: - """Test import flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - }, - data={ - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - }, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "channel123" - assert "result" in result - assert "token" in result["result"].data - assert result["result"].data["token"]["access_token"] == "efgh" - assert result["result"].data["token"]["refresh_token"] == "" - assert result["result"].unique_id == "123" - assert result["options"] == {CONF_CHANNELS: ["channel123"]} - - -@pytest.mark.parametrize("twitch_mock", [TwitchInvalidTokenMock()]) -async def test_import_invalid_token( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, - mock_setup_entry, - twitch: TwitchMock, -) -> None: - """Test import flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - }, - data={ - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - }, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "invalid_token" - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_import_already_imported( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, - config_entry: MockConfigEntry, - mock_setup_entry, - twitch: TwitchMock, -) -> None: - """Test import flow where the config is already imported.""" - await setup_integration(hass, config_entry) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - }, - data={ - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - }, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 diff --git a/tests/components/twitch/test_sensor.py b/tests/components/twitch/test_sensor.py index 3385cb228fd..bb6624f7847 100644 --- a/tests/components/twitch/test_sensor.py +++ b/tests/components/twitch/test_sensor.py @@ -4,12 +4,7 @@ from datetime import datetime import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.twitch.const import CONF_CHANNELS, DOMAIN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component from ...common import MockConfigEntry from . import ( @@ -23,58 +18,6 @@ from . import ( ) ENTITY_ID = "sensor.channel123" -CONFIG = { - "auth_implementation": "cred", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", -} - -LEGACY_CONFIG_WITHOUT_TOKEN = { - SENSOR_DOMAIN: { - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - "channels": ["channel123"], - } -} - -LEGACY_CONFIG = { - SENSOR_DOMAIN: { - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - } -} - -OPTIONS = {CONF_CHANNELS: ["channel123"]} - - -async def test_legacy_migration( - hass: HomeAssistant, twitch: TwitchMock, mock_setup_entry -) -> None: - """Test importing legacy yaml.""" - 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 - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_legacy_migration_without_token( - hass: HomeAssistant, twitch: TwitchMock -) -> None: - """Test importing legacy yaml.""" - assert await async_setup_component( - hass, Platform.SENSOR, LEGACY_CONFIG_WITHOUT_TOKEN - ) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 0 - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 async def test_offline( diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 6d9ce7b7f72..ba37f188079 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -56,10 +56,10 @@ async def test_state(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -73,7 +73,7 @@ async def test_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { "region": "1", @@ -87,10 +87,10 @@ async def test_state_district(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -98,7 +98,7 @@ async def test_state_district(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -112,7 +112,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { "region": "2.2", @@ -126,10 +126,10 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -137,7 +137,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -151,7 +151,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { "region": "2", @@ -165,12 +165,12 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -178,7 +178,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3", }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -186,7 +186,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3.2", }, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -200,7 +200,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { "region": "3.2.1", @@ -221,7 +221,7 @@ async def test_max_regions(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "max_regions" @@ -231,7 +231,7 @@ async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "rate_limit" @@ -243,7 +243,7 @@ async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -253,7 +253,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -265,7 +265,7 @@ async def test_unknown_client_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -275,7 +275,7 @@ async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "timeout" @@ -287,5 +287,5 @@ async def test_no_regions_returned( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index d5be861139b..8f9838e3e37 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -1,20 +1,64 @@ """UniFi Network button platform tests.""" +from datetime import timedelta + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass from homeassistant.components.unifi.const import CONF_SITE_ID +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_HOST, + CONTENT_TYPE_JSON, STATE_UNAVAILABLE, EntityCategory, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import RegistryEntryDisabler +import homeassistant.util.dt as dt_util from .test_hub import setup_unifi_integration +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker +WLAN_ID = "_id" +WLAN = { + WLAN_ID: "012345678910111213141516", + "bc_filter_enabled": False, + "bc_filter_list": [], + "dtim_mode": "default", + "dtim_na": 1, + "dtim_ng": 1, + "enabled": True, + "group_rekey": 3600, + "mac_filter_enabled": False, + "mac_filter_list": [], + "mac_filter_policy": "allow", + "minrate_na_advertising_rates": False, + "minrate_na_beacon_rate_kbps": 6000, + "minrate_na_data_rate_kbps": 6000, + "minrate_na_enabled": False, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_ng_advertising_rates": False, + "minrate_ng_beacon_rate_kbps": 1000, + "minrate_ng_data_rate_kbps": 1000, + "minrate_ng_enabled": False, + "minrate_ng_mgmt_rate_kbps": 1000, + "name": "SSID 1", + "no2ghz_oui": False, + "schedule": [], + "security": "wpapsk", + "site_id": "5a32aa4ee4b0412345678910", + "usergroup_id": "012345678910111213141518", + "wep_idx": 1, + "wlangroup_id": "012345678910111213141519", + "wpa_enc": "ccmp", + "wpa_mode": "wpa2", + "x_iapp_key": "01234567891011121314151617181920", + "x_passphrase": "password", +} + async def test_restart_device_button( hass: HomeAssistant, @@ -168,3 +212,71 @@ async def test_power_cycle_poe( assert ( hass.states.get("button.switch_port_1_power_cycle").state != STATE_UNAVAILABLE ) + + +async def test_wlan_regenerate_password( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + websocket_mock, +) -> None: + """Test WLAN regenerate password button.""" + + config_entry = await setup_unifi_integration( + hass, aioclient_mock, wlans_response=[WLAN] + ) + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 0 + + button_regenerate_password = "button.ssid_1_regenerate_password" + + ent_reg_entry = entity_registry.async_get(button_regenerate_password) + assert ent_reg_entry.unique_id == "regenerate_password-012345678910111213141516" + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.CONFIG + + # Enable entity + entity_registry.async_update_entity( + entity_id=button_regenerate_password, disabled_by=None + ) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 + + # Validate state object + button = hass.states.get(button_regenerate_password) + assert button is not None + assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.UPDATE + + aioclient_mock.clear_requests() + aioclient_mock.put( + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{WLAN[WLAN_ID]}", + json={"data": "password changed successfully", "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + # Send WLAN regenerate password command + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {"entity_id": button_regenerate_password}, + blocking=True, + ) + assert aioclient_mock.call_count == 1 + assert next(iter(aioclient_mock.mock_calls[0][2])) == "x_passphrase" + + # Availability signalling + + # Controller disconnects + await websocket_mock.disconnect() + assert hass.states.get(button_regenerate_password).state == STATE_UNAVAILABLE + + # Controller reconnects + await websocket_mock.reconnect() + assert hass.states.get(button_regenerate_password).state != STATE_UNAVAILABLE diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index ee309ca2579..b269392f707 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import aiounifi -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.unifi.config_flow import _async_discover_unifi from homeassistant.components.unifi.const import ( @@ -33,6 +33,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .test_hub import setup_unifi_integration @@ -105,7 +106,7 @@ async def test_flow_works( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "unifi", @@ -145,7 +146,7 @@ async def test_flow_works( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Site name" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -165,7 +166,7 @@ async def test_flow_works_negative_discovery( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "", @@ -184,7 +185,7 @@ async def test_flow_multiple_sites( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -218,7 +219,7 @@ async def test_flow_multiple_sites( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "site" assert result["data_schema"]({"site": "1"}) assert result["data_schema"]({"site": "2"}) @@ -234,7 +235,7 @@ async def test_flow_raise_already_configured( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -269,7 +270,7 @@ async def test_flow_raise_already_configured( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -291,7 +292,7 @@ async def test_flow_aborts_configuration_updated( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -325,7 +326,7 @@ async def test_flow_aborts_configuration_updated( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "configuration_updated" @@ -337,7 +338,7 @@ async def test_flow_fails_user_credentials_faulty( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -354,7 +355,7 @@ async def test_flow_fails_user_credentials_faulty( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "faulty_credentials"} @@ -366,7 +367,7 @@ async def test_flow_fails_hub_unavailable( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -383,7 +384,7 @@ async def test_flow_fails_hub_unavailable( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "service_unavailable"} @@ -405,7 +406,7 @@ async def test_reauth_flow_update_configuration( data=config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -440,7 +441,7 @@ async def test_reauth_flow_update_configuration( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == "1.2.3.4" assert config_entry.data[CONF_USERNAME] == "new_name" @@ -465,7 +466,7 @@ async def test_advanced_option_flow( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_entity_sources" assert not result["last_step"] assert list(result["data_schema"].schema[CONF_CLIENT_SOURCE].options.keys()) == [ @@ -476,7 +477,7 @@ async def test_advanced_option_flow( user_input={CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"]}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_tracker" assert not result["last_step"] assert list(result["data_schema"].schema[CONF_SSID_FILTER].options.keys()) == [ @@ -498,7 +499,7 @@ async def test_advanced_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "client_control" assert not result["last_step"] @@ -510,7 +511,7 @@ async def test_advanced_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "statistics_sensors" assert result["last_step"] @@ -522,7 +523,7 @@ async def test_advanced_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"], CONF_TRACK_CLIENTS: False, @@ -550,7 +551,7 @@ async def test_simple_option_flow( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "simple_options" assert result["last_step"] @@ -563,7 +564,7 @@ async def test_simple_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, @@ -580,7 +581,7 @@ async def test_option_flow_integration_not_setup( hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "integration_not_setup" @@ -601,7 +602,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -646,7 +647,7 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass: HomeAssistant) -> N }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -673,7 +674,7 @@ async def test_form_ssdp_aborts_if_serial_already_exists(hass: HomeAssistant) -> }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -700,7 +701,7 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass: HomeAssistant) -> No }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} context = next( diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 9053b47cbaf..bd9a29f2c8b 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -3,10 +3,20 @@ from typing import Any from unittest.mock import patch +from aiounifi.models.message import MessageKey + +from homeassistant import loader from homeassistant.components import unifi -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi.const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + DOMAIN as UNIFI_DOMAIN, +) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .test_hub import DEFAULT_CONFIG_ENTRY_ID, setup_unifi_integration @@ -103,3 +113,91 @@ async def test_wireless_clients( "00:00:00:00:00:01", "00:00:00:00:00:02", ] + + +async def test_remove_config_entry_device( + hass: HomeAssistant, + hass_storage: dict[str, Any], + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, + mock_unifi_websocket, +) -> None: + """Verify removing a device manually.""" + client_1 = { + "hostname": "Wired client", + "is_wired": True, + "mac": "00:00:00:00:00:01", + "oui": "Producer", + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + "uptime": 1600094505, + } + client_2 = { + "is_wired": False, + "mac": "00:00:00:00:00:02", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes": 2345000000, + "tx_bytes": 6789000000, + "uptime": 60, + } + device_1 = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device 1", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + } + options = { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + CONF_TRACK_CLIENTS: True, + CONF_TRACK_DEVICES: True, + } + + config_entry = await setup_unifi_integration( + hass, + aioclient_mock, + options=options, + clients_response=[client_1, client_2], + devices_response=[device_1], + ) + + integration = await loader.async_get_integration(hass, config_entry.domain) + component = await integration.async_get_component() + + # Remove a client + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[client_2]) + await hass.async_block_till_done() + + # Try to remove an active client: not allowed + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, client_1["mac"])} + ) + assert not await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ) + # Try to remove an active device: not allowed + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, device_1["mac"])} + ) + assert not await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ) + # Try to remove an inactive client: allowed + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, client_2["mac"])} + ) + assert await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 7a58252a6bd..26eadfa498e 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -835,8 +835,8 @@ async def test_outlet_power_readings( """Test the outlet power reporting on PDU devices.""" await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) - assert len(hass.states.async_all()) == 11 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5 + assert len(hass.states.async_all()) == 13 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7 ent_reg_entry = entity_registry.async_get(f"sensor.{entity_id}") assert ent_reg_entry.unique_id == expected_unique_id @@ -998,3 +998,196 @@ async def test_device_state( device["state"] = i mock_unifi_websocket(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i] + + +async def test_device_system_stats( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, +) -> None: + """Verify that device stats sensors are working as expected.""" + + device = { + "device_id": "mock-id", + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "state": 1, + "version": "4.0.42.10433", + "system-stats": {"cpu": 5.8, "mem": 31.1, "uptime": 7316}, + } + + await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) + + assert len(hass.states.async_all()) == 8 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + + assert hass.states.get("sensor.device_cpu_utilization").state == "5.8" + assert hass.states.get("sensor.device_memory_utilization").state == "31.1" + + assert ( + entity_registry.async_get("sensor.device_cpu_utilization").entity_category + is EntityCategory.DIAGNOSTIC + ) + + assert ( + entity_registry.async_get("sensor.device_memory_utilization").entity_category + is EntityCategory.DIAGNOSTIC + ) + + # Verify new event change system-stats + device["system-stats"] = {"cpu": 7.7, "mem": 33.3, "uptime": 7316} + mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + + assert hass.states.get("sensor.device_cpu_utilization").state == "7.7" + assert hass.states.get("sensor.device_memory_utilization").state == "33.3" + + +async def test_bandwidth_port_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, +) -> None: + """Verify that port bandwidth sensors are working as expected.""" + device_reponse = { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "port_table": [ + { + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_class": "Class 4", + "poe_enable": False, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": False, + "up": True, + "rx_bytes-r": 1151, + "tx_bytes-r": 5111, + }, + { + "media": "GE", + "name": "Port 2", + "port_idx": 2, + "poe_class": "Class 4", + "poe_enable": False, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a2", + "port_poe": False, + "up": True, + "rx_bytes-r": 1536, + "tx_bytes-r": 3615, + }, + ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + options = { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: False, + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + } + + config_entry = await setup_unifi_integration( + hass, + aioclient_mock, + options=options, + devices_response=[device_reponse], + ) + + assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + p1rx_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_rx") + assert p1rx_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert p1rx_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + p1tx_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_tx") + assert p1tx_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert p1tx_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_1_rx", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_1_tx", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_2_rx", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_2_tx", disabled_by=None + ) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + # Validate state object + assert len(hass.states.async_all()) == 9 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 + + # Verify sensor attributes and state + p1rx_sensor = hass.states.get("sensor.mock_name_port_1_rx") + assert p1rx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p1rx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p1rx_sensor.state == "0.00921" + + p1tx_sensor = hass.states.get("sensor.mock_name_port_1_tx") + assert p1tx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p1tx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p1tx_sensor.state == "0.04089" + + p2rx_sensor = hass.states.get("sensor.mock_name_port_2_rx") + assert p2rx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p2rx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p2rx_sensor.state == "0.01229" + + p2tx_sensor = hass.states.get("sensor.mock_name_port_2_tx") + assert p2tx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p2tx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p2tx_sensor.state == "0.02892" + + # Verify state update + device_reponse["port_table"][0]["rx_bytes-r"] = 3456000000 + device_reponse["port_table"][0]["tx_bytes-r"] = 7891000000 + + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_reponse) + await hass.async_block_till_done() + + assert hass.states.get("sensor.mock_name_port_1_rx").state == "27648.00000" + assert hass.states.get("sensor.mock_name_port_1_tx").state == "63128.00000" + + # Disable option + options[CONF_ALLOW_BANDWIDTH_SENSORS] = False + hass.config_entries.async_update_entry(config_entry, options=options.copy()) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + assert hass.states.get("sensor.mock_name_uptime") + assert hass.states.get("sensor.mock_name_state") + assert hass.states.get("sensor.mock_name_port_1_rx") is None + assert hass.states.get("sensor.mock_name_port_1_tx") is None + assert hass.states.get("sensor.mock_name_port_2_rx") is None + assert hass.states.get("sensor.mock_name_port_2_tx") is None diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 7c9f584af15..845766809b2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.unifiprotect.const import ( CONF_OVERRIDE_CHOST, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -63,7 +64,7 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] bootstrap.nvr = nvr @@ -91,7 +92,7 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "1.1.1.1", @@ -127,7 +128,7 @@ async def test_form_version_too_old( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "protect_version"} @@ -150,7 +151,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -178,7 +179,7 @@ async def test_form_cloud_user( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cloud_user"} @@ -201,7 +202,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -230,7 +231,7 @@ async def test_form_reauth_auth( "entry_id": mock_config.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -250,7 +251,7 @@ async def test_form_reauth_auth( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "reauth_confirm" @@ -274,7 +275,7 @@ async def test_form_reauth_auth( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 @@ -307,10 +308,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() - assert mock_config.state == config_entries.ConfigEntryState.LOADED + assert mock_config.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "init" @@ -323,7 +324,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "all_updates": True, "disable_rtsp": True, @@ -331,6 +332,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "max_media": 1000, "allow_ea_channel": False, } + await hass.async_block_till_done() await hass.config_entries.async_unload(mock_config.entry_id) @@ -354,7 +356,7 @@ async def test_discovered_by_ssdp_or_dhcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_started" @@ -371,7 +373,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -405,7 +407,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DIRECT_CONNECT_DOMAIN, @@ -446,7 +448,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN @@ -484,7 +486,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "127.0.0.1" @@ -522,7 +524,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "1.2.2.2" @@ -553,7 +555,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "a.hostname" @@ -571,7 +573,7 @@ async def test_discovered_by_unifi_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -605,7 +607,7 @@ async def test_discovered_by_unifi_discovery( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -632,7 +634,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -666,7 +668,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -706,7 +708,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -736,7 +738,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -777,7 +779,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -814,7 +816,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -848,7 +850,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "nomatchsameip.ui.direct", @@ -892,7 +894,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -913,5 +915,5 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index f123abb9861..0e3fd42e28b 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -94,7 +94,7 @@ async def test_setup_multiple( await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() - assert mock_config.state == ConfigEntryState.LOADED + assert mock_config.state is ConfigEntryState.LOADED assert ufp.api.update.called assert mock_config.unique_id == ufp.api.bootstrap.nvr.mac @@ -158,7 +158,7 @@ async def test_setup_cloud_account( await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.entry.state is ConfigEntryState.LOADED await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 9df9247900f..008f7aa5162 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -7,12 +7,9 @@ import pytest from voluptuous.error import MultipleInvalid from homeassistant import config as hass_config -import homeassistant.components.input_number as input_number -import homeassistant.components.input_select as input_select -import homeassistant.components.media_player as media_player +from homeassistant.components import input_number, input_select, media_player, switch from homeassistant.components.media_player import MediaClass, MediaPlayerEntityFeature from homeassistant.components.media_player.browse_media import BrowseMedia -import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal from homeassistant.const import ( SERVICE_RELOAD, diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index b3c3dfce15c..5eaed2e3a24 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, PropertyMock, patch from homeassistant import config_entries from homeassistant.components.upb.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def mocked_upb(sync_complete=True, config_ok=True): @@ -33,11 +34,10 @@ async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( flow["flow_id"], {"protocol": "TCP", "address": "1.2.3.4", "file_path": "upb.upe"}, ) - return result async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: @@ -63,9 +63,9 @@ async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "UPB" assert result["data"] == { "host": "serial:///dev/ttyS0:115200", @@ -77,7 +77,7 @@ async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: async def test_form_user_with_tcp_upb(hass: HomeAssistant) -> None: """Test we can setup a serial upb.""" result = await valid_tcp_flow(hass) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"host": "tcp://1.2.3.4", "file_path": "upb.upe"} await hass.async_block_till_done() @@ -92,14 +92,14 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ): result = await valid_tcp_flow(hass, sync_complete=False) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} async def test_form_missing_upb_file(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await valid_tcp_flow(hass, config_ok=False) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_upb_file"} @@ -107,7 +107,7 @@ async def test_form_user_with_already_configured(hass: HomeAssistant) -> None: """Test we can setup a TCP upb.""" _ = await valid_tcp_flow(hass) result2 = await valid_tcp_flow(hass) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" await hass.async_block_till_done() @@ -128,7 +128,7 @@ async def test_form_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "UPB" assert result["data"] == {"host": "tcp://42.4.2.42", "file_path": "upb.upe"} @@ -145,7 +145,7 @@ async def test_form_junk_input(hass: HomeAssistant) -> None: data={"foo": "goo", "goo": "foo"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} await hass.async_block_till_done() diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 86f73463fab..4ce87bf38ab 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -44,7 +44,7 @@ async def test_connection_error( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -65,7 +65,7 @@ async def test_login_error( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -80,7 +80,7 @@ async def test_success( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -129,7 +129,7 @@ async def test_already_configured(hass: HomeAssistant, requests_mock) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=new_user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_USERNAME] == new_user_input[CONF_USERNAME] assert config_entry.data[CONF_PASSWORD] == new_user_input[CONF_PASSWORD] diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 31a9ee7b36e..6ece4f818d1 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.update import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, EntityCategory @@ -214,15 +214,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "update_available {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "update_available {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -238,15 +235,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "no_update {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "no_update {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -314,15 +308,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "no_update {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "no_update {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -383,15 +374,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 67b4e5b10e6..a3d2b97f3ed 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_HOST, @@ -19,6 +19,7 @@ from homeassistant.components.upnp.const import ( ST_IGD_V1, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( TEST_DISCOVERY, @@ -48,7 +49,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -56,7 +57,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -82,7 +83,7 @@ async def test_flow_ssdp_ignore(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Ignore entry. @@ -91,7 +92,7 @@ async def test_flow_ssdp_ignore(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -121,7 +122,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -143,7 +144,7 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "non_igd_device" @@ -161,7 +162,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -169,7 +170,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -209,7 +210,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -241,7 +242,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant) - context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -283,7 +284,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs( context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # UDN + ST different: New discovery via step ssdp. @@ -301,7 +302,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs( context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" @@ -333,7 +334,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant) -> None context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test if location is updated. @@ -362,7 +363,7 @@ async def test_flow_ssdp_discovery_ignored_entry(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -395,7 +396,7 @@ async def test_flow_ssdp_discovery_changed_udn_ignored_entry( context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_ignored" @@ -411,7 +412,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Confirmed via step user. @@ -419,7 +420,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input={"unique_id": TEST_USN}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -442,7 +443,7 @@ async def test_flow_user_no_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -463,7 +464,7 @@ async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=test_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -471,7 +472,7 @@ async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index a2234882b27..2c9c4edacde 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -21,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -29,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -44,5 +44,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 23c0d3e1ce7..c2d154cd967 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -16,6 +16,7 @@ from pyuptimerobot import ( from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant @@ -116,6 +117,6 @@ async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON assert hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY).state == STATE_UP - assert mock_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 58faa524d6f..1cf0a358a87 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -75,7 +75,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "not_main_key" @@ -102,7 +102,7 @@ async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == error_key @@ -137,7 +137,7 @@ async def test_user_unique_id_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -157,7 +157,7 @@ async def test_user_unique_id_already_exists( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -178,7 +178,7 @@ async def test_reauthentication( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -198,7 +198,7 @@ async def test_reauthentication( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -219,7 +219,7 @@ async def test_reauthentication_failure( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -240,7 +240,7 @@ async def test_reauthentication_failure( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -263,7 +263,7 @@ async def test_reauthentication_failure_no_existing_entry( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -283,7 +283,7 @@ async def test_reauthentication_failure_no_existing_entry( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" @@ -304,7 +304,7 @@ async def test_reauthentication_failure_account_not_matching( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -328,5 +328,5 @@ async def test_reauthentication_failure_account_not_matching( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index a45480e50a5..c0583eddb7d 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -11,6 +11,7 @@ from homeassistant.components.uptimerobot.const import ( COORDINATOR_UPDATE_INTERVAL, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -44,7 +45,7 @@ async def test_reauthentication_trigger_in_setup( flows = hass.config_entries.flow.async_progress() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert mock_config_entry.reason == "could not authenticate" assert len(flows) == 1 @@ -74,7 +75,7 @@ async def test_reauthentication_trigger_key_read_only( flows = hass.config_entries.flow.async_progress() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert ( mock_config_entry.reason == "Wrong API key type detected, use the 'main' API key" @@ -102,7 +103,7 @@ async def test_reauthentication_trigger_after_setup( mock_config_entry = await setup_uptimerobot_integration(hass) binary_sensor = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert binary_sensor.state == STATE_ON with patch( @@ -146,7 +147,7 @@ async def test_integration_reload( await hass.async_block_till_done() entry = hass.config_entries.async_get_entry(mock_entry.entry_id) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON diff --git a/tests/components/usb/__init__.py b/tests/components/usb/__init__.py index f5f32336931..96d671d0958 100644 --- a/tests/components/usb/__init__.py +++ b/tests/components/usb/__init__.py @@ -26,3 +26,19 @@ electro_lama_device = USBDevice( manufacturer=None, description="USB2.0-Serial", ) +skyconnect_macos_correct = USBDevice( + device="/dev/cu.SLAB_USBtoUART", + vid="10C4", + pid="EA60", + serial_number="9ab1da1ea4b3ed11956f4eaca7669f5d", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", +) +skyconnect_macos_incorrect = USBDevice( + device="/dev/cu.usbserial-2110", + vid="10C4", + pid="EA60", + serial_number="9ab1da1ea4b3ed11956f4eaca7669f5d", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", +) diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 1bf1c02385d..b5553b1efe7 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -21,7 +21,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -40,7 +40,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -79,7 +79,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -94,7 +94,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -127,7 +127,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -142,7 +142,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "tariffs_not_unique" @@ -153,7 +153,7 @@ async def test_non_periodically_resetting(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -169,7 +169,7 @@ async def test_non_periodically_resetting(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -206,7 +206,7 @@ async def test_always_available(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -223,7 +223,7 @@ async def test_always_available(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -290,7 +290,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "source") == input_sensor1_entity_id @@ -304,7 +304,7 @@ async def test_options(hass: HomeAssistant) -> None: "always_available": True, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "cycle": "monthly", "delta_values": False, @@ -428,7 +428,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( utility_meter_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -436,7 +436,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: "source": current_entity_source.entity_id, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() # Confirm that the configuration entry has been removed from the source entity 1 (previous) device registry @@ -458,7 +458,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( utility_meter_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -466,7 +466,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: "source": current_entity_source.entity_id, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() # Confirm that the configuration entry has been removed from the source entity 2 (previous) device registry @@ -490,7 +490,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( utility_meter_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -498,7 +498,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: "source": current_entity_source.entity_id, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() # Confirm that the configuration entry has been added to the source entity 2 (current) device registry diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 43a71eca85e..cd0a8082578 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1339,7 +1339,7 @@ async def test_delta_values( await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill") - assert state.attributes.get("status") == PAUSED + assert state.attributes.get("status") == COLLECTING now += timedelta(seconds=30) with freeze_time(now): @@ -1382,7 +1382,7 @@ async def test_delta_values( state = hass.states.get("sensor.energy_bill") assert state is not None - assert state.state == "9" + assert state.state == "10" @pytest.mark.parametrize( @@ -1449,7 +1449,7 @@ async def test_non_periodically_resetting( await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill") - assert state.attributes.get("status") == PAUSED + assert state.attributes.get("status") == COLLECTING now += timedelta(seconds=30) with freeze_time(now): diff --git a/tests/components/v2c/test_config_flow.py b/tests/components/v2c/test_config_flow.py index a0657fa0c7c..04cf66d1d58 100644 --- a/tests/components/v2c/test_config_flow.py +++ b/tests/components/v2c/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "EVSE 1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -65,7 +65,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} with patch( @@ -80,7 +80,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "EVSE 1.1.1.1" assert result3["data"] == { "host": "1.1.1.1", diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index fe5b2814a33..fec2ca1bf12 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index f8d1368a163..850c69c1757 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import ( DOMAIN, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 831d6807b8c..bae57b1941f 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import DOMAIN, STATE_CLEANING, STATE_DOCKED from homeassistant.const import EntityCategory @@ -356,15 +356,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index 00c11854fe2..cfeb7152b17 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form_no_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None @@ -30,7 +30,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert init["type"] == FlowResultType.FORM + assert init["type"] is FlowResultType.FORM assert init["errors"] is None with ( @@ -49,7 +49,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -67,7 +67,7 @@ async def test_form_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "invalid_host"} @@ -87,7 +87,7 @@ async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -107,7 +107,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -127,7 +127,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "unknown"} @@ -152,5 +152,5 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index a00d975f0eb..eee215d2e29 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -54,7 +54,7 @@ class MockValveEntity(ValveEntity): unique_id: str = "mock_valve", name: str = "Valve", features: ValveEntityFeature = ValveEntityFeature(0), - current_position: int = None, + current_position: int | None = None, device_class: ValveDeviceClass = None, reports_position: bool = True, ) -> None: @@ -104,7 +104,7 @@ class MockBinaryValveEntity(ValveEntity): unique_id: str = "mock_valve_2", name: str = "Valve", features: ValveEntityFeature = ValveEntityFeature(0), - is_closed: bool = None, + is_closed: bool | None = None, ) -> None: """Initialize the valve.""" self._attr_name = name @@ -205,7 +205,7 @@ async def test_valve_setup( assert 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 is ConfigEntryState.LOADED for entity in mock_config_entry[1]: entity_id = entity.entity_id state = hass.states.get(entity_id) @@ -215,7 +215,7 @@ async def test_valve_setup( assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED for entity in mock_config_entry[1]: entity_id = entity.entity_id diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 4e3eeaf0fb8..79d67415c4f 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -7,12 +7,12 @@ import pytest import serial.tools.list_ports from velbusaio.exceptions import VelbusConnectionFailed -from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import PORT_SERIAL, PORT_TCP @@ -73,7 +73,7 @@ async def test_user(hass: HomeAssistant) -> None: ) assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # try with a serial port @@ -83,7 +83,7 @@ async def test_user(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_serial" data = result.get("data") assert data @@ -96,7 +96,7 @@ async def test_user(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_tcp" data = result.get("data") assert data @@ -112,7 +112,7 @@ async def test_user_fail(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_PORT: "cannot_connect"} result = await hass.config_entries.flow.async_init( @@ -121,7 +121,7 @@ async def test_user_fail(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_PORT: "cannot_connect"} @@ -134,7 +134,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -148,7 +148,7 @@ async def test_flow_usb(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -156,7 +156,7 @@ async def test_flow_usb(hass: HomeAssistant) -> None: user_input={}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -170,7 +170,7 @@ async def test_flow_usb(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -184,5 +184,5 @@ async def test_flow_usb_failed(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index 08efdd0410b..dea246b8a86 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -20,12 +20,12 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/velux/test_config_flow.py b/tests/components/velux/test_config_flow.py index 816dbf95420..8021ad52810 100644 --- a/tests/components/velux/test_config_flow.py +++ b/tests/components/velux/test_config_flow.py @@ -9,11 +9,11 @@ from unittest.mock import patch import pytest from pyvlx import PyVLXException -from homeassistant import data_entry_flow from homeassistant.components.velux import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -45,7 +45,7 @@ async def test_user_success(hass: HomeAssistant) -> None: client_mock.return_value.disconnect.assert_called_once() client_mock.return_value.connect.assert_called_once() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DUMMY_DATA[CONF_HOST] assert result["data"] == DUMMY_DATA @@ -64,7 +64,7 @@ async def test_user_errors( connect_mock.assert_called_once() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error_name} @@ -77,7 +77,7 @@ async def test_import_valid_config(hass: HomeAssistant) -> None: context={"source": SOURCE_IMPORT}, data=DUMMY_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DUMMY_DATA[CONF_HOST] assert result["data"] == DUMMY_DATA @@ -97,7 +97,7 @@ async def test_flow_duplicate_entry(hass: HomeAssistant, flow_source: str) -> No context={"source": flow_source}, data=DUMMY_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,5 +115,5 @@ async def test_import_errors( context={"source": SOURCE_IMPORT}, data=DUMMY_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error_name diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py index 077f87975f0..b560a75c8a8 100644 --- a/tests/components/venstar/test_config_flow.py +++ b/tests/components/venstar/test_config_flow.py @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -76,7 +76,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -95,7 +95,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -107,7 +107,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -126,5 +126,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index 75250b52f5b..bc8d400df6c 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -54,11 +54,11 @@ async def test_setup_entry(hass: HomeAssistant) -> None: 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 is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_setup_entry_exception(hass: HomeAssistant) -> None: @@ -97,4 +97,4 @@ async def test_setup_entry_exception(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index b6be60927cf..af21bf5d3a3 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -58,8 +58,8 @@ class ControllerConfig(NamedTuple): def new_simple_controller_config( - config: dict = None, - options: dict = None, + config: dict | None = None, + options: dict | None = None, config_source=ConfigSource.CONFIG_FLOW, serial_number="1111", devices: tuple[pv.VeraDevice, ...] = (), diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 2262347450d..057945450e3 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from requests.exceptions import RequestException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -25,7 +25,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result = await hass.config_entries.flow.async_configure( @@ -36,7 +36,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_EXCLUDE: "14 15", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -65,7 +65,7 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -95,7 +95,7 @@ async def test_async_step_import_success_with_legacy_unique_id( data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -118,7 +118,7 @@ async def test_async_step_finish_error(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert result["description_placeholders"] == { "base_url": "http://127.0.0.1:123" @@ -139,7 +139,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -149,7 +149,7 @@ async def test_options(hass: HomeAssistant) -> None: CONF_EXCLUDE: "8,9;10 11 12_13bb14", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index ebe8beb4e29..c31845b80af 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -20,8 +20,8 @@ async def run_sensor_test( category: int, class_property: str, assert_states: tuple[tuple[Any, Any]], - assert_unit_of_measurement: str = None, - setup_callback: Callable[[pv.VeraController], None] = None, + assert_unit_of_measurement: str | None = None, + setup_callback: Callable[[pv.VeraController], None] | None = None, ) -> None: """Test generic sensor.""" vera_device: pv.VeraSensor = MagicMock(spec=pv.VeraSensor) diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 62ae00b5622..cf478b093c0 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -32,7 +32,7 @@ async def test_full_user_flow_single_installation( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.get_installations.return_value = { @@ -49,7 +49,7 @@ async def test_full_user_flow_single_installation( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "ascending (12345th street)" assert result2.get("data") == { CONF_GIID: "12345", @@ -71,7 +71,7 @@ async def test_full_user_flow_multiple_installations( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} result2 = await hass.config_entries.flow.async_configure( @@ -84,7 +84,7 @@ async def test_full_user_flow_multiple_installations( await hass.async_block_till_done() assert result2.get("step_id") == "installation" - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") is None result3 = await hass.config_entries.flow.async_configure( @@ -92,7 +92,7 @@ async def test_full_user_flow_multiple_installations( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "descending (54321th street)" assert result3.get("data") == { CONF_GIID: "54321", @@ -114,7 +114,7 @@ async def test_full_user_flow_single_installation_with_mfa( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -130,7 +130,7 @@ async def test_full_user_flow_single_installation_with_mfa( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "mfa" mock_verisure_config_flow.login.side_effect = None @@ -147,7 +147,7 @@ async def test_full_user_flow_single_installation_with_mfa( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "ascending (12345th street)" assert result3.get("data") == { CONF_GIID: "12345", @@ -171,7 +171,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -187,7 +187,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "mfa" mock_verisure_config_flow.login.side_effect = None @@ -201,7 +201,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( await hass.async_block_till_done() assert result3.get("step_id") == "installation" - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("errors") is None result4 = await hass.config_entries.flow.async_configure( @@ -209,7 +209,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( ) await hass.async_block_till_done() - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "descending (54321th street)" assert result4.get("data") == { CONF_GIID: "54321", @@ -252,7 +252,7 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": error} @@ -272,7 +272,7 @@ async def test_verisure_errors( mock_verisure_config_flow.request_mfa.side_effect = None - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "user" assert result3.get("errors") == {"base": "unknown_mfa"} @@ -285,7 +285,7 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4.get("step_id") == "mfa" mock_verisure_config_flow.validate_mfa.side_effect = side_effect @@ -296,7 +296,7 @@ async def test_verisure_errors( "code": "123456", }, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5.get("step_id") == "mfa" assert result5.get("errors") == {"base": error} @@ -315,7 +315,7 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("type") is FlowResultType.CREATE_ENTRY assert result6.get("title") == "ascending (12345th street)" assert result6.get("data") == { CONF_GIID: "12345", @@ -339,7 +339,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" @@ -362,7 +362,7 @@ async def test_reauth_flow( data=mock_config_entry.data, ) assert result.get("step_id") == "reauth_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} result2 = await hass.config_entries.flow.async_configure( @@ -374,7 +374,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_GIID: "12345", @@ -405,7 +405,7 @@ async def test_reauth_flow_with_mfa( data=mock_config_entry.data, ) assert result.get("step_id") == "reauth_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -421,7 +421,7 @@ async def test_reauth_flow_with_mfa( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_mfa" mock_verisure_config_flow.login.side_effect = None @@ -434,7 +434,7 @@ async def test_reauth_flow_with_mfa( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_GIID: "12345", @@ -487,7 +487,7 @@ async def test_reauth_flow_errors( await hass.async_block_till_done() assert result2.get("step_id") == "reauth_confirm" - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": error} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -504,7 +504,7 @@ async def test_reauth_flow_errors( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "reauth_confirm" assert result3.get("errors") == {"base": "unknown_mfa"} @@ -519,7 +519,7 @@ async def test_reauth_flow_errors( ) await hass.async_block_till_done() - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4.get("step_id") == "reauth_mfa" mock_verisure_config_flow.validate_mfa.side_effect = side_effect @@ -530,7 +530,7 @@ async def test_reauth_flow_errors( "code": "123456", }, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5.get("step_id") == "reauth_mfa" assert result5.get("errors") == {"base": error} @@ -575,7 +575,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result = await hass.config_entries.options.async_configure( @@ -583,5 +583,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_LOCK_CODE_DIGITS: 4}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_LOCK_CODE_DIGITS: DEFAULT_LOCK_CODE_DIGITS} diff --git a/tests/components/version/common.py b/tests/components/version/common.py index 33a1747cf0e..cd9469d08a1 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -15,6 +15,7 @@ from homeassistant.components.version.const import ( UPDATE_COORDINATOR_UPDATE_INTERVAL, VERSION_SOURCE_LOCAL, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -42,7 +43,7 @@ async def mock_get_version_update( freezer: FrozenDateTimeFactory, version: str = MOCK_VERSION, data: dict[str, Any] = MOCK_VERSION_DATA, - side_effect: Exception = None, + side_effect: Exception | None = None, ) -> None: """Mock getting version.""" with patch( @@ -75,6 +76,6 @@ async def setup_version_integration( assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index d7edb5526d5..edf3439644d 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.version.const import ( VERSION_SOURCE_PYPI, VERSION_SOURCE_VERSIONS, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -31,7 +32,7 @@ from tests.common import async_fire_time_changed async def test_reload_config_entry(hass: HomeAssistant) -> None: """Test reloading the config entry.""" config_entry = await setup_version_integration(hass) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED with patch( "pyhaversion.HaVersion.get_version", @@ -44,7 +45,7 @@ async def test_reload_config_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() entry = hass.config_entries.async_get_entry(config_entry.entry_id) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_basic_form(hass: HomeAssistant) -> None: @@ -53,7 +54,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.version.async_setup_entry", @@ -65,7 +66,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == VERSION_SOURCE_DOCKER_HUB assert result2["data"] == { **DEFAULT_CONFIGURATION, @@ -81,7 +82,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,11 +90,11 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -105,7 +106,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_PYPI assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -122,7 +123,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -130,11 +131,11 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -146,7 +147,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_DOCKER_HUB assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -163,7 +164,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -171,11 +172,11 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -188,7 +189,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev" assert result["data"] == { **DEFAULT_CONFIGURATION, diff --git a/tests/components/vesync/common.py b/tests/components/vesync/common.py index 23c57177ddd..94e1511ce19 100644 --- a/tests/components/vesync/common.py +++ b/tests/components/vesync/common.py @@ -75,22 +75,21 @@ def call_api_side_effect__no_devices(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200 - elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": + if args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": return ( json.loads( load_fixture("vesync_api_call__devices__no_devices.json", "vesync") ), 200, ) - else: - raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") + raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") def call_api_side_effect__single_humidifier(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200 - elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": + if args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": return ( json.loads( load_fixture( @@ -99,7 +98,7 @@ def call_api_side_effect__single_humidifier(*args, **kwargs): ), 200, ) - elif args[0] == "/cloud/v2/deviceManaged/bypassV2" and kwargs["method"] == "post": + if args[0] == "/cloud/v2/deviceManaged/bypassV2" and kwargs["method"] == "post": return ( json.loads( load_fixture( @@ -108,22 +107,21 @@ def call_api_side_effect__single_humidifier(*args, **kwargs): ), 200, ) - else: - raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") + raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") def call_api_side_effect__single_fan(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200 - elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": + if args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": return ( json.loads( load_fixture("vesync_api_call__devices__single_fan.json", "vesync") ), 200, ) - elif ( + if ( args[0] == "/131airPurifier/v1/device/deviceDetail" and kwargs["method"] == "post" ): @@ -135,5 +133,4 @@ def call_api_side_effect__single_fan(*args, **kwargs): ), 200, ) - else: - raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") + raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index 23e0938cce6..5500ef1a55f 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -71,15 +71,13 @@ def manager_fixture() -> VeSync: @pytest.fixture(name="fan") def fan_fixture(): """Create a mock VeSync fan fixture.""" - mock_fixture = Mock(VeSyncAirBypass) - return mock_fixture + return Mock(VeSyncAirBypass) @pytest.fixture(name="bulb") def bulb_fixture(): """Create a mock VeSync bulb fixture.""" - mock_fixture = Mock(VeSyncBulb) - return mock_fixture + return Mock(VeSyncBulb) @pytest.fixture(name="switch") @@ -101,5 +99,4 @@ def dimmable_switch_fixture(): @pytest.fixture(name="outlet") def outlet_fixture(): """Create a mock VeSync outlet fixture.""" - mock_fixture = Mock(VeSyncOutlet) - return mock_fixture + return Mock(VeSyncOutlet) diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index a283b89b841..22a93e1ba56 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.vesync import DOMAIN, config_flow 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 @@ -19,7 +19,7 @@ async def test_abort_already_setup(hass: HomeAssistant) -> None: ) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -31,7 +31,7 @@ async def test_invalid_login_error(hass: HomeAssistant) -> None: with patch("pyvesync.vesync.VeSync.login", return_value=False): result = await flow.async_step_user(user_input=test_dict) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -40,11 +40,11 @@ async def test_config_flow_user_input(hass: HomeAssistant) -> None: flow = config_flow.VeSyncFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("pyvesync.vesync.VeSync.login", return_value=True): result = await flow.async_step_user( {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == "user" assert result["data"][CONF_PASSWORD] == "pass" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 031fcdff9d3..edef1606572 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -43,7 +43,7 @@ async def test_user_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -59,7 +59,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -73,7 +73,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -88,7 +88,7 @@ async def test_user_create_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ViCare" assert result["data"] == snapshot mock_setup_entry.assert_called_once() @@ -109,7 +109,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, data=VALID_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # test PyViCareInvalidConfigurationError @@ -123,7 +123,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> result["flow_id"], user_input={CONF_PASSWORD: new_password, CONF_CLIENT_ID: new_client_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -136,7 +136,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> result["flow_id"], user_input={CONF_PASSWORD: new_password, CONF_CLIENT_ID: new_client_id}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -159,7 +159,7 @@ async def test_form_dhcp( context={"source": SOURCE_DHCP}, data=DHCP_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -173,7 +173,7 @@ async def test_form_dhcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ViCare" assert result["data"] == snapshot mock_setup_entry.assert_called_once() @@ -192,7 +192,7 @@ async def test_dhcp_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=DHCP_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -208,5 +208,5 @@ async def test_user_input_single_instance_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/vilfo/conftest.py b/tests/components/vilfo/conftest.py new file mode 100644 index 00000000000..75ed352c839 --- /dev/null +++ b/tests/components/vilfo/conftest.py @@ -0,0 +1,61 @@ +"""Vilfo tests conftest.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.vilfo import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.vilfo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_vilfo_client() -> Generator[AsyncMock, None, None]: + """Mock a Vilfo client.""" + with patch( + "homeassistant.components.vilfo.config_flow.VilfoClient", + autospec=True, + ) as mock_client: + client = mock_client.return_value + client.get_board_information.return_value = None + client.ping.return_value = None + client.resolve_firmware_version.return_value = "1.1.0" + client.resolve_mac_address.return_value = "FF-00-00-00-00-00" + client.mac = "FF-00-00-00-00-00" + yield client + + +@pytest.fixture +def mock_is_valid_host() -> Generator[AsyncMock, None, None]: + """Mock is_valid_host.""" + with patch( + "homeassistant.components.vilfo.config_flow.is_host_valid", + return_value=True, + ) as mock_is_valid_host: + yield mock_is_valid_host + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="testadmin.vilfo.com", + unique_id="FF-00-00-00-00-00", + data={ + CONF_HOST: "testadmin.vilfo.com", + CONF_ACCESS_TOKEN: "test-token", + }, + ) diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index c1755f95043..c4fdb2fe22c 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,218 +1,191 @@ """Test the Vilfo Router config flow.""" -from unittest.mock import Mock, patch +from typing import Any +from unittest.mock import AsyncMock -import vilfo +import pytest +from vilfo.exceptions import AuthenticationException, VilfoException -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.config_entries import SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - - mock_mac = "FF-00-00-00-00-00" - firmware_version = "1.1.0" - 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("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"], +@pytest.mark.parametrize( + ("user_input", "expected_unique_id", "mac"), + [ + ( {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, - ) - await hass.async_block_till_done() + "testadmin.vilfo.com", + None, + ), + ( + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + "FF-00-00-00-00-00", + "FF-00-00-00-00-00", + ), + ( + {CONF_HOST: "192.168.0.1", CONF_ACCESS_TOKEN: "test-token"}, + "FF-00-00-00-00-00", + "FF-00-00-00-00-00", + ), + ( + {CONF_HOST: "2001:db8::1428:57ab", CONF_ACCESS_TOKEN: "test-token"}, + "FF-00-00-00-00-00", + "FF-00-00-00-00-00", + ), + ], +) +async def test_full_flow( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_is_valid_host: AsyncMock, + user_input: dict[str, Any], + expected_unique_id: str, + mac: str | None, +) -> None: + """Test we can finish a config flow.""" - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result2["title"] == "testadmin.vilfo.com" - assert result2["data"] == { - "host": "testadmin.vilfo.com", - "access_token": "test-token", - } + mock_vilfo_client.resolve_mac_address.return_value = mac + mock_vilfo_client.mac = mac + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == user_input[CONF_HOST] + assert result["data"] == user_input + assert result["result"].unique_id == expected_unique_id assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass: HomeAssistant) -> None: +async def test_form_invalid_auth( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_is_valid_host: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: """Test we handle invalid auth.""" + mock_vilfo_client.get_board_information.side_effect = AuthenticationException + mock_vilfo_client.resolve_mac_address.return_value = None + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_vilfo_client.get_board_information.side_effect = None + mock_vilfo_client.resolve_mac_address.return_value = "FF-00-00-00-00-00" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [(VilfoException, "cannot_connect"), (Exception, "unknown")], +) +async def test_form_exceptions( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_is_valid_host: AsyncMock, + mock_setup_entry: AsyncMock, + side_effect: Exception, + error: str, +) -> None: + """Test we handle exceptions.""" + mock_vilfo_client.ping.side_effect = side_effect + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": 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, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -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} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, ) - 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"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + mock_vilfo_client.ping.side_effect = None - 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"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM - assert result3["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_form_wrong_host(hass: HomeAssistant) -> None: +async def test_form_wrong_host( + hass: HomeAssistant, + mock_is_valid_host: AsyncMock, +) -> None: """Test we handle wrong host errors.""" + mock_is_valid_host.return_value = False result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={"host": "this is an invalid hostname", "access_token": "test-token"}, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: "this is an invalid hostname", + CONF_ACCESS_TOKEN: "test-token", + }, ) assert result["errors"] == {"host": "wrong_host"} -async def test_form_already_configured(hass: HomeAssistant) -> None: +async def test_form_already_configured( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_is_valid_host: AsyncMock, +) -> None: """Test that we handle already configured exceptions appropriately.""" - first_flow_result1 = await hass.config_entries.flow.async_init( - 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), - ): - 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"}, - ) + mock_config_entry.add_to_hass(hass) - second_flow_result1 = await hass.config_entries.flow.async_init( - 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), - ): - 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"}, - ) - - assert first_flow_result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert second_flow_result2["type"] == data_entry_flow.FlowResultType.ABORT - assert second_flow_result2["reason"] == "already_configured" - - -async def test_form_unexpected_exception(hass: HomeAssistant) -> None: - """Test that we handle unexpected exceptions.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) - - with patch( - "homeassistant.components.vilfo.config_flow.VilfoClient", - ) as mock_client: - mock_client.return_value.ping = Mock(side_effect=Exception) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) - - assert result2["errors"] == {"base": "unknown"} - - -async def test_validate_input_returns_data(hass: HomeAssistant) -> None: - """Test we handle the MAC address being resolved or not.""" - mock_data = {"host": "testadmin.vilfo.com", "access_token": "test-token"} - mock_data_with_ip = {"host": "192.168.0.1", "access_token": "test-token"} - mock_data_with_ipv6 = {"host": "2001:db8::1428:57ab", "access_token": "test-token"} - 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 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 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"] - assert result2[CONF_MAC] == mock_mac - assert result2[CONF_ID] == mock_mac - - assert result3["title"] == mock_data_with_ip["host"] - assert result3[CONF_HOST] == mock_data_with_ip["host"] - assert result3[CONF_MAC] == mock_mac - assert result3[CONF_ID] == mock_mac - - assert result4["title"] == mock_data_with_ipv6["host"] - assert result4[CONF_HOST] == mock_data_with_ipv6["host"] - assert result4[CONF_MAC] == mock_mac - assert result4[CONF_ID] == mock_mac + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 6ce36b38c8f..783ed8b4585 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -37,7 +37,7 @@ class MockInput: def get_mock_inputs(input_list): """Return list of MockInput.""" - return [MockInput(input) for input in input_list] + return [MockInput(device_input) for device_input in input_list] @pytest.fixture(name="vizio_get_unique_id", autouse=True) diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index d19cf319a5a..712dd2a31b5 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -5,7 +5,6 @@ import dataclasses import pytest import voluptuous as vol -from homeassistant import data_entry_flow from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( @@ -32,6 +31,7 @@ from homeassistant.const import ( CONF_PIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( ACCESS_TOKEN, @@ -67,14 +67,14 @@ async def test_user_flow_minimum_fields( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -92,14 +92,14 @@ async def test_user_flow_all_fields( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -118,19 +118,19 @@ async def test_speaker_options_flow( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -146,12 +146,12 @@ async def test_tv_options_flow_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -161,7 +161,7 @@ async def test_tv_options_flow_no_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -177,12 +177,12 @@ async def test_tv_options_flow_with_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -192,7 +192,7 @@ async def test_tv_options_flow_with_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -209,13 +209,13 @@ async def test_tv_options_flow_start_with_volume( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init( entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} @@ -224,7 +224,7 @@ async def test_tv_options_flow_start_with_volume( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -234,7 +234,7 @@ async def test_tv_options_flow_start_with_volume( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -261,7 +261,7 @@ async def test_user_host_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -285,7 +285,7 @@ async def test_user_serial_number_already_exists( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -297,7 +297,7 @@ async def test_user_error_on_could_not_connect( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -309,7 +309,7 @@ async def test_user_error_on_could_not_connect_invalid_token( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -324,19 +324,19 @@ async def test_user_tv_pairing_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing_complete" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -355,7 +355,7 @@ async def test_user_start_pairing_failure( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -371,14 +371,14 @@ async def test_user_invalid_pin( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" assert result["errors"] == {CONF_PIN: "complete_pairing_failed"} @@ -400,7 +400,7 @@ async def test_user_ignore( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_import_flow_minimum_fields( @@ -417,7 +417,7 @@ async def test_import_flow_minimum_fields( ), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST @@ -437,7 +437,7 @@ async def test_import_flow_all_fields( data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -464,7 +464,7 @@ async def test_import_entity_already_configured( DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" @@ -482,7 +482,7 @@ async def test_import_flow_update_options( await hass.async_block_till_done() assert result["result"].options == {CONF_VOLUME_STEP: DEFAULT_VOLUME_STEP} - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_SPEAKER_CONFIG.copy() @@ -493,7 +493,7 @@ async def test_import_flow_update_options( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 @@ -513,7 +513,7 @@ async def test_import_flow_update_name_and_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() @@ -525,7 +525,7 @@ async def test_import_flow_update_name_and_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.data[CONF_NAME] == NAME2 @@ -547,7 +547,7 @@ async def test_import_flow_update_remove_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS in config_entry.options @@ -560,7 +560,7 @@ async def test_import_flow_update_remove_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" assert CONF_APPS not in config_entry.data assert CONF_APPS not in config_entry.options @@ -577,26 +577,26 @@ async def test_import_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -617,7 +617,7 @@ async def test_import_with_apps_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=import_config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Mock inputting info without apps to make sure apps get stored @@ -626,19 +626,19 @@ async def test_import_with_apps_needs_pairing( user_input=_get_config_schema(MOCK_TV_CONFIG_NO_TOKEN)(MOCK_TV_CONFIG_NO_TOKEN), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -660,7 +660,7 @@ async def test_import_flow_additional_configs( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS not in config_entry.options @@ -689,7 +689,7 @@ async def test_import_error( data=vol.Schema(VIZIO_SCHEMA)(fail_entry), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM # Ensure error gets logged vizio_log_list = [ @@ -720,7 +720,7 @@ async def test_import_ignore( data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_flow( @@ -736,7 +736,7 @@ async def test_zeroconf_flow( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Apply discovery updates to entry to mimic when user hits submit without changing @@ -753,7 +753,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == NAME @@ -782,7 +782,7 @@ async def test_zeroconf_flow_already_configured( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -810,7 +810,7 @@ async def test_zeroconf_flow_with_port_in_host( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -827,7 +827,7 @@ async def test_zeroconf_dupe_fail( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) @@ -836,7 +836,7 @@ async def test_zeroconf_dupe_fail( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -860,7 +860,7 @@ async def test_zeroconf_ignore( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -875,7 +875,7 @@ async def test_zeroconf_no_unique_id( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -900,7 +900,7 @@ async def test_zeroconf_abort_when_ignored( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -929,7 +929,7 @@ async def test_zeroconf_flow_already_configured_hostname( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -954,7 +954,7 @@ async def test_import_flow_already_configured_hostname( ) # Flow should abort because device was updated - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" assert entry.data[CONF_HOST] == HOST diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index f5207c52c99..54edafab14a 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -51,7 +51,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -69,7 +69,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_data["host"] assert result["data"] == entry_data assert len(mock_setup_entry.mock_calls) == 1 @@ -94,7 +94,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non data=entry_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -137,7 +137,7 @@ async def test_errors( {"password": "test-password"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -178,7 +178,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert dict(entry.data) == {**entry_data, "password": "new-password"} @@ -237,7 +237,7 @@ async def test_reauth_errors( {"password": "test-password"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -272,11 +272,11 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == test_data.config["name"] assert result2["data"] == test_data.config assert len(mock_setup_entry.mock_calls) == 1 @@ -303,7 +303,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT @pytest.mark.parametrize( @@ -352,9 +352,9 @@ async def test_hassio_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == error diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index f2619044861..0492d32070f 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from aiovodafone import exceptions as aiovodafone_exceptions import pytest -from homeassistant import data_entry_flow from homeassistant.components.device_tracker import CONF_CONSIDER_HOME from homeassistant.components.vodafone_station.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER @@ -34,13 +33,13 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_USERNAME] == "fake_username" assert result["data"][CONF_PASSWORD] == "fake_password" @@ -66,7 +65,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -77,7 +76,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is not None assert result["errors"]["base"] == error @@ -111,7 +110,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "fake_host" assert result2["data"] == { "host": "fake_host", @@ -143,7 +142,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -154,7 +153,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -191,7 +190,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -201,7 +200,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is not None assert result["errors"]["base"] == error @@ -233,7 +232,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -253,7 +252,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_CONSIDER_HOME: 37, } diff --git a/tests/components/voip/conftest.py b/tests/components/voip/conftest.py index 619a80d86c4..bcd9becbc5a 100644 --- a/tests/components/voip/conftest.py +++ b/tests/components/voip/conftest.py @@ -38,7 +38,7 @@ async def setup_voip(hass: HomeAssistant, config_entry: MockConfigEntry) -> None return_value=(Mock(), AsyncMock()), ): assert await async_setup_component(hass, DOMAIN, {}) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED yield diff --git a/tests/components/voip/test_config_flow.py b/tests/components/voip/test_config_flow.py index 079177db139..1b7aaad7c03 100644 --- a/tests/components/voip/test_config_flow.py +++ b/tests/components/voip/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import voip from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -16,7 +16,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( voip.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -29,7 +29,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -41,7 +41,7 @@ async def test_single_instance( result = await hass.config_entries.flow.async_init( voip.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -59,7 +59,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( config_entry.entry_id, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # Default @@ -67,7 +67,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"sip_port": 5060} # Manual @@ -78,5 +78,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: result["flow_id"], user_input={"sip_port": 5061}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"sip_port": 5061} diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 7d185161d0a..9c3708f970c 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant.components import zeroconf from homeassistant.components.volumio.config_flow import CannotConnectError from homeassistant.components.volumio.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -62,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TestVolumio" assert result2["data"] == {**TEST_SYSTEM_INFO, **TEST_CONNECTION} @@ -103,7 +104,7 @@ async def test_form_updates_unique_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert entry.data == {**TEST_SYSTEM_INFO, **TEST_CONNECTION} @@ -114,7 +115,7 @@ async def test_empty_system_info(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -133,7 +134,7 @@ async def test_empty_system_info(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_CONNECTION["host"] assert result2["data"] == { "host": TEST_CONNECTION["host"], @@ -160,7 +161,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_CONNECTION, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -179,7 +180,7 @@ async def test_form_exception(hass: HomeAssistant) -> None: TEST_CONNECTION, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -206,7 +207,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DISCOVERY_RESULT["name"] assert result2["data"] == TEST_DISCOVERY_RESULT @@ -232,7 +233,7 @@ async def test_discovery_cannot_connect(hass: HomeAssistant) -> None: user_input={}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "cannot_connect" @@ -241,13 +242,13 @@ async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -278,7 +279,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data == TEST_DISCOVERY_RESULT diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py index 3c866da58ea..8bf8bcc7412 100644 --- a/tests/components/volvooncall/test_config_flow.py +++ b/tests/components/volvooncall/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(result["errors"]) == 0 with ( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -74,7 +74,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -86,7 +86,7 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(result["errors"]) == 0 with ( @@ -108,7 +108,7 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -133,7 +133,7 @@ async def test_form_other_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -162,7 +162,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) # the first form is just the confirmation prompt - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -171,7 +171,7 @@ async def test_reauth(hass: HomeAssistant) -> None: await hass.async_block_till_done() # the second form is the user flow where reauth happens - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("volvooncall.Connection.get"): result3 = await hass.config_entries.flow.async_configure( @@ -186,5 +186,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index b0b928cfde2..01c6bf3edaf 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -14,11 +14,12 @@ from vulcan import ( ) from vulcan.model import Student -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.vulcan import config_flow, const, register from homeassistant.components.vulcan.config_flow import ClientConnectionError, Keystore from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_fixture @@ -38,7 +39,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -58,7 +59,7 @@ async def test_config_flow_auth_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -72,7 +73,7 @@ async def test_config_flow_auth_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -97,7 +98,7 @@ async def test_config_flow_auth_success_with_multiple_students( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -106,7 +107,7 @@ async def test_config_flow_auth_success_with_multiple_students( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -119,7 +120,7 @@ async def test_config_flow_auth_success_with_multiple_students( {"student": "0"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -145,7 +146,7 @@ async def test_config_flow_reauth_success( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -158,7 +159,7 @@ async def test_config_flow_reauth_success( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -184,7 +185,7 @@ async def test_config_flow_reauth_without_matching_entries( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -193,7 +194,7 @@ async def test_config_flow_reauth_without_matching_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_matching_entries" @@ -208,7 +209,7 @@ async def test_config_flow_reauth_with_errors( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} with patch( @@ -220,7 +221,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_token"} @@ -233,7 +234,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "expired_token"} @@ -246,7 +247,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_pin"} @@ -259,7 +260,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_symbol"} @@ -272,7 +273,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -285,7 +286,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "unknown"} @@ -312,7 +313,7 @@ async def test_multiple_config_entries( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -321,7 +322,7 @@ async def test_multiple_config_entries( {"use_saved_credentials": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -334,7 +335,7 @@ async def test_multiple_config_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -357,7 +358,7 @@ async def test_multiple_config_entries_using_saved_credentials( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -370,7 +371,7 @@ async def test_multiple_config_entries_using_saved_credentials( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -394,7 +395,7 @@ async def test_multiple_config_entries_using_saved_credentials_2( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -403,7 +404,7 @@ async def test_multiple_config_entries_using_saved_credentials_2( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -416,7 +417,7 @@ async def test_multiple_config_entries_using_saved_credentials_2( {"student": "0"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -447,7 +448,7 @@ async def test_multiple_config_entries_using_saved_credentials_3( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -456,7 +457,7 @@ async def test_multiple_config_entries_using_saved_credentials_3( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -469,7 +470,7 @@ async def test_multiple_config_entries_using_saved_credentials_3( {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -501,7 +502,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -510,7 +511,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -519,7 +520,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -532,7 +533,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( {"student": "0"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -559,7 +560,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -571,7 +572,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials( "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=UnauthorizedCertificateException, ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -580,7 +581,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials( {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_credentials"} @@ -607,7 +608,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -619,7 +620,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=ClientConnectionError, ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -628,7 +629,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -655,7 +656,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -667,7 +668,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=Exception, ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -676,7 +677,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -706,7 +707,7 @@ async def test_student_already_exists( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -715,7 +716,7 @@ async def test_student_already_exists( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "all_student_already_configured" @@ -733,7 +734,7 @@ async def test_config_flow_auth_invalid_token( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -742,7 +743,7 @@ async def test_config_flow_auth_invalid_token( {CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_token"} @@ -761,7 +762,7 @@ async def test_config_flow_auth_invalid_region( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -770,7 +771,7 @@ async def test_config_flow_auth_invalid_region( {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_symbol"} @@ -787,7 +788,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass: HomeAssistant) const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -796,7 +797,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass: HomeAssistant) {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_pin"} @@ -815,7 +816,7 @@ async def test_config_flow_auth_expired_token( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -824,7 +825,7 @@ async def test_config_flow_auth_expired_token( {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_token"} @@ -843,7 +844,7 @@ async def test_config_flow_auth_connection_error( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -852,7 +853,7 @@ async def test_config_flow_auth_connection_error( {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -871,7 +872,7 @@ async def test_config_flow_auth_unknown_error( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -880,6 +881,6 @@ async def test_config_flow_auth_unknown_error( {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 0aac011d02a..1e957ad7a2c 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -165,9 +165,9 @@ async def test_config_entry_unload( ) -> None: """Test we can unload config entry.""" config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @freeze_time("2023-06-22 10:30:00+00:00") @@ -268,7 +268,7 @@ async def test_restore_state( config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert state.state == timestamp diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index ebb3a2fd693..c0ff0b19c94 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -5,7 +5,7 @@ import json import requests_mock -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.wallbox import config_flow from homeassistant.components.wallbox.const import ( CHARGER_ADDED_ENERGY_KEY, @@ -17,7 +17,9 @@ from homeassistant.components.wallbox.const import ( CHARGER_MAX_CHARGING_CURRENT_KEY, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( authorisation_response, @@ -47,7 +49,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: flow.hass = hass result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -77,7 +79,7 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -107,7 +109,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -144,7 +146,7 @@ async def test_form_validate_input(hass: HomeAssistant) -> None: async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test we handle reauth flow.""" await setup_integration(hass, entry) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -175,16 +177,17 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" + await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test we handle reauth invalid flow.""" await setup_integration(hass, entry) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -215,7 +218,7 @@ async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry) }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "reauth_invalid"} await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 3dfc391aa3b..f1362489c50 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -28,10 +28,10 @@ async def test_wallbox_setup_unload_entry( """Test Wallbox Unload.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_unload_entry_connection_error( @@ -40,10 +40,10 @@ async def test_wallbox_unload_entry_connection_error( """Test Wallbox Unload Connection Error.""" await setup_integration_connection_error(hass, entry) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_connection_error_auth( @@ -52,7 +52,7 @@ async def test_wallbox_refresh_failed_connection_error_auth( """Test Wallbox setup with connection error.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -71,7 +71,7 @@ async def test_wallbox_refresh_failed_connection_error_auth( await wallbox.async_refresh() assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_invalid_auth( @@ -80,7 +80,7 @@ async def test_wallbox_refresh_failed_invalid_auth( """Test Wallbox setup with authentication error.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -99,7 +99,7 @@ async def test_wallbox_refresh_failed_invalid_auth( await wallbox.async_refresh() assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_connection_error( @@ -108,7 +108,7 @@ async def test_wallbox_refresh_failed_connection_error( """Test Wallbox setup with connection error.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -127,7 +127,7 @@ async def test_wallbox_refresh_failed_connection_error( await wallbox.async_refresh() assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_read_only( @@ -136,7 +136,7 @@ async def test_wallbox_refresh_failed_read_only( """Test Wallbox setup for read-only user.""" await setup_integration_read_only(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/waqi/test_config_flow.py b/tests/components/waqi/test_config_flow.py index 712ad8dd39e..fecac7ea0bd 100644 --- a/tests/components/waqi/test_config_flow.py +++ b/tests/components/waqi/test_config_flow.py @@ -52,7 +52,7 @@ async def test_full_map_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -71,7 +71,7 @@ async def test_full_map_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == method with ( @@ -97,7 +97,7 @@ async def test_full_map_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "de Jongweg, Utrecht" assert result["data"] == { CONF_API_KEY: "asd", @@ -137,7 +137,7 @@ async def test_flow_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with ( @@ -157,7 +157,7 @@ async def test_flow_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "map" with ( @@ -179,7 +179,7 @@ async def test_flow_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -231,7 +231,7 @@ async def test_error_in_second_step( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -250,7 +250,7 @@ async def test_error_in_second_step( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == method with ( @@ -266,7 +266,7 @@ async def test_error_in_second_step( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with ( @@ -292,7 +292,7 @@ async def test_error_in_second_step( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "de Jongweg, Utrecht" assert result["data"] == { CONF_API_KEY: "asd", diff --git a/tests/components/waqi/test_sensor.py b/tests/components/waqi/test_sensor.py index 328fe99330e..0825d65cc20 100644 --- a/tests/components/waqi/test_sensor.py +++ b/tests/components/waqi/test_sensor.py @@ -52,4 +52,4 @@ async def test_updating_failed( assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index d456fa7be71..e08721d3e10 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.water_heater import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index c8e4ed5b06b..f8eee6b48bf 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.watttime.config_flow import ( CONF_LOCATION_TYPE, LOCATION_TYPE_HOME, @@ -41,7 +41,7 @@ async def test_auth_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_auth ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} @@ -76,7 +76,7 @@ async def test_coordinate_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors @@ -93,7 +93,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -104,13 +104,13 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -128,7 +128,7 @@ async def test_show_form_coordinates( result["flow_id"], user_input=config_location_type ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "coordinates" assert result["errors"] is None @@ -138,7 +138,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -164,7 +164,7 @@ async def test_step_reauth( user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -186,7 +186,7 @@ async def test_step_user_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", @@ -211,7 +211,7 @@ async def test_step_user_home( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 9bd5016c2f4..5b1e3417bfc 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -2,7 +2,7 @@ import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.waze_travel_time.const import ( CONF_AVOID_FERRIES, CONF_AVOID_SUBSCRIPTION_ROADS, @@ -21,6 +21,7 @@ from homeassistant.components.waze_travel_time.const import ( ) from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import CONFIG_FLOW_USER_INPUT, MOCK_CONFIG @@ -33,7 +34,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -42,7 +43,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -52,6 +53,50 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: } +@pytest.mark.usefixtures("mock_update") +async def test_reconfigure(hass: HomeAssistant) -> None: + """Test reconfigure flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + options=DEFAULT_OPTIONS, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + reconfigure_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "user" + + user_step_result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ORIGIN: "location3", + CONF_DESTINATION: "location4", + CONF_REGION: "us", + }, + ) + assert user_step_result["type"] is FlowResultType.ABORT + assert user_step_result["reason"] == "reconfigure_successful" + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: DEFAULT_NAME, + CONF_ORIGIN: "location3", + CONF_DESTINATION: "location4", + CONF_REGION: "US", + } + + async def test_options(hass: HomeAssistant) -> None: """Test options flow.""" entry = MockConfigEntry( @@ -65,7 +110,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -81,7 +126,7 @@ async def test_options(hass: HomeAssistant) -> None: CONF_VEHICLE_TYPE: "taxi", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_AVOID_FERRIES: True, @@ -112,7 +157,7 @@ async def test_dupe(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -121,13 +166,13 @@ async def test_dupe(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -136,7 +181,7 @@ async def test_dupe(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("invalidate_config_entry") @@ -147,14 +192,14 @@ async def test_invalid_config_entry( 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["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG_FLOW_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Error trying to validate entry" in caplog.text diff --git a/tests/components/weatherflow/test_config_flow.py b/tests/components/weatherflow/test_config_flow.py index 51aec02cab7..f46c1cbf75a 100644 --- a/tests/components/weatherflow/test_config_flow.py +++ b/tests/components/weatherflow/test_config_flow.py @@ -30,7 +30,7 @@ async def test_single_instance( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -48,7 +48,7 @@ async def test_devices_with_mocks( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -80,12 +80,12 @@ async def test_devices_with_various_mocks_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == error_msg assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} diff --git a/tests/components/weatherflow_cloud/test_config_flow.py b/tests/components/weatherflow_cloud/test_config_flow.py index cef0e224434..b111ef462e6 100644 --- a/tests/components/weatherflow_cloud/test_config_flow.py +++ b/tests/components/weatherflow_cloud/test_config_flow.py @@ -17,7 +17,7 @@ async def test_config(hass: HomeAssistant, mock_get_stations) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -27,7 +27,7 @@ async def test_config(hass: HomeAssistant, mock_get_stations) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None: @@ -43,7 +43,7 @@ async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -51,7 +51,7 @@ async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -71,7 +71,7 @@ async def test_config_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -80,7 +80,7 @@ async def test_config_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": expected_error} with mock_get_stations: @@ -90,7 +90,7 @@ async def test_config_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_reauth(hass: HomeAssistant, mock_get_stations_401_error) -> None: @@ -110,7 +110,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_stations_401_error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, @@ -118,4 +118,4 @@ async def test_reauth(hass: HomeAssistant, mock_get_stations_401_error) -> None: ) assert result["reason"] == "reauth_successful" - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/weatherkit/test_config_flow.py b/tests/components/weatherkit/test_config_flow.py index 58397ac2ed0..396889dd815 100644 --- a/tests/components/weatherkit/test_config_flow.py +++ b/tests/components/weatherkit/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -59,7 +59,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY location = EXAMPLE_USER_INPUT[CONF_LOCATION] assert result["title"] == f"{location[CONF_LATITUDE]}, {location[CONF_LONGITUDE]}" @@ -94,7 +94,7 @@ async def test_error_handling( EXAMPLE_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": expected_error} @@ -113,7 +113,7 @@ async def test_form_unsupported_location(hass: HomeAssistant) -> None: EXAMPLE_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unsupported_location"} # Test that we can recover from this error by changing the location @@ -126,7 +126,7 @@ async def test_form_unsupported_location(hass: HomeAssistant) -> None: EXAMPLE_USER_INPUT, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -159,7 +159,7 @@ async def test_auto_fix_key_input( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -174,7 +174,7 @@ async def test_auto_fix_key_input( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_KEY_PEM] == EXAMPLE_CONFIG_DATA[CONF_KEY_PEM] assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/weatherkit/test_setup.py b/tests/components/weatherkit/test_setup.py index c121f0cc5c1..f198a81b894 100644 --- a/tests/components/weatherkit/test_setup.py +++ b/tests/components/weatherkit/test_setup.py @@ -7,8 +7,8 @@ from apple_weatherkit.client import ( WeatherKitApiClientError, ) -from homeassistant import config_entries from homeassistant.components.weatherkit.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import EXAMPLE_CONFIG_DATA @@ -65,4 +65,4 @@ async def test_client_error_handling(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/webmin/test_config_flow.py b/tests/components/webmin/test_config_flow.py index e680f0e164a..a9f5eafc5c7 100644 --- a/tests/components/webmin/test_config_flow.py +++ b/tests/components/webmin/test_config_flow.py @@ -28,7 +28,7 @@ async def user_flow(hass: HomeAssistant) -> str: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None return result["flow_id"] @@ -47,7 +47,7 @@ async def test_form_user( user_flow, TEST_USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER_INPUT[CONF_HOST] assert result["options"] == TEST_USER_INPUT @@ -89,7 +89,7 @@ async def test_form_user_errors( user_flow, TEST_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error_type} @@ -101,7 +101,7 @@ async def test_form_user_errors( result["flow_id"], TEST_USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER_INPUT[CONF_HOST] assert result["options"] == TEST_USER_INPUT @@ -121,7 +121,7 @@ async def test_duplicate_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER_INPUT[CONF_HOST] assert result["options"] == TEST_USER_INPUT @@ -137,5 +137,5 @@ async def test_duplicate_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 07a11b5bf29..afda36d913f 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -55,7 +55,7 @@ async def test_form(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_init( @@ -65,7 +65,7 @@ async def test_form(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -74,7 +74,7 @@ async def test_form(hass: HomeAssistant, client) -> None: await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TV_NAME @@ -106,7 +106,7 @@ async def test_options_flow_live_tv_in_apps( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -115,7 +115,7 @@ async def test_options_flow_live_tv_in_apps( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] @@ -127,7 +127,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve"} @@ -145,7 +145,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -163,7 +163,7 @@ async def test_form_pairexception(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "error_pairing" @@ -178,7 +178,7 @@ async def test_entry_already_configured(hass: HomeAssistant, client) -> None: data=MOCK_USER_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -191,7 +191,7 @@ async def test_form_ssdp(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" @@ -206,7 +206,7 @@ async def test_ssdp_in_progress(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result2 = await hass.config_entries.flow.async_init( @@ -214,7 +214,7 @@ async def test_ssdp_in_progress(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -229,7 +229,7 @@ async def test_ssdp_update_uuid(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] @@ -248,7 +248,7 @@ async def test_ssdp_not_update_uuid(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" assert entry.unique_id is None @@ -266,7 +266,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_config = { @@ -281,7 +281,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -290,7 +290,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" @@ -309,7 +309,7 @@ async def test_reauth_successful(hass: HomeAssistant, client, monkeypatch) -> No result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY @@ -318,7 +318,7 @@ async def test_reauth_successful(hass: HomeAssistant, client, monkeypatch) -> No result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_CLIENT_SECRET] == "new_key" @@ -346,7 +346,7 @@ async def test_reauth_errors( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) @@ -354,5 +354,5 @@ async def test_reauth_errors( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index 30af1428701..a2961a81a4e 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -18,7 +18,7 @@ async def test_reauth_setup_entry(hass: HomeAssistant, client, monkeypatch) -> N monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) entry = await setup_webostv(hass) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -37,5 +37,5 @@ async def test_key_update_setup_entry(hass: HomeAssistant, client, monkeypatch) monkeypatch.setattr(client, "client_key", "new_key") entry = await setup_webostv(hass) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.data[CONF_CLIENT_SECRET] == "new_key" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 2dff9477e50..6608c107599 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -794,12 +794,12 @@ async def test_reauth_reconnect(hass: HomeAssistant, client, monkeypatch) -> Non monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 52d0e86d828..655d8adf1ea 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -8,7 +8,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import pytest import voluptuous as vol -from homeassistant import config_entries, loader +from homeassistant import loader from homeassistant.components.device_automation import toggle_entity from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( @@ -17,6 +17,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import Context, HomeAssistant, State, SupportsResponse, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -203,7 +204,7 @@ async def test_return_response_error(hass: HomeAssistant, websocket_client) -> N assert msg["id"] == 8 assert msg["type"] == const.TYPE_RESULT assert not msg["success"] - assert msg["error"]["code"] == "unknown_error" + assert msg["error"]["code"] == "service_validation_error" @pytest.mark.parametrize("command", ["call_service", "call_service_action"]) @@ -700,7 +701,7 @@ async def test_get_services( assert msg["id"] == id_ assert msg["type"] == const.TYPE_RESULT assert msg["success"] - assert msg["result"] == hass.services.async_services() + assert msg["result"].keys() == hass.services.async_services().keys() async def test_get_config( @@ -2433,7 +2434,7 @@ async def test_execute_script_with_dynamically_validated_action( ) config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index 3e9c13a8b15..0ade5329190 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -1,5 +1,7 @@ """Test decorators.""" +import voluptuous as vol + from homeassistant.components import http, websocket_api from homeassistant.core import HomeAssistant @@ -31,9 +33,16 @@ async def test_async_response_request_context( def get_request(hass, connection, msg): handle_request(http.current_request.get(), connection, msg) + @websocket_api.websocket_command( + {"type": "test-get-request-with-arg", vol.Required("arg"): str} + ) + def get_with_arg_request(hass, connection, msg): + handle_request(http.current_request.get(), connection, msg) + websocket_api.async_register_command(hass, executor_get_request) websocket_api.async_register_command(hass, async_get_request) websocket_api.async_register_command(hass, get_request) + websocket_api.async_register_command(hass, get_with_arg_request) await websocket_client.send_json( { @@ -71,6 +80,65 @@ async def test_async_response_request_context( assert not msg["success"] assert msg["error"]["code"] == "not_found" + await websocket_client.send_json( + { + "id": 8, + "type": "test-get-request-with-arg", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 8 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" + assert ( + msg["error"]["message"] == "required key not provided @ data['arg']. Got None" + ) + + await websocket_client.send_json( + { + "id": 9, + "type": "test-get-request-with-arg", + "arg": "dog", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 9 + assert msg["success"] + assert msg["result"] == "/api/websocket" + + await websocket_client.send_json( + { + "id": -1, + "type": "test-get-request-with-arg", + "arg": "dog", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == -1 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" + assert msg["error"]["message"] == "Message incorrectly formatted." + + await websocket_client.send_json( + { + "id": 10, + "type": "test-get-request", + "not_valid": "dog", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 10 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" + assert msg["error"]["message"] == ( + "extra keys not allowed. " + "Got {'id': 10, 'type': 'test-get-request', 'not_valid': 'dog'}" + ) + async def test_supervisor_only(hass: HomeAssistant, websocket_client) -> None: """Test that only the Supervisor can make requests.""" diff --git a/tests/components/wemo/__init__.py b/tests/components/wemo/__init__.py index 33bdcacd37d..68d1516aed6 100644 --- a/tests/components/wemo/__init__.py +++ b/tests/components/wemo/__init__.py @@ -1 +1,5 @@ """Tests for the wemo component.""" + +import pytest + +pytest.register_assert_rewrite("tests.components.wemo.entity_test_helpers") diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py index 5cb2b54c9a0..6eaa32b960e 100644 --- a/tests/components/wemo/test_config_flow.py +++ b/tests/components/wemo/test_config_flow.py @@ -21,7 +21,7 @@ async def test_not_discovered(hass: HomeAssistant) -> None: with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: mock_pywemo.discover_devices.return_value = [] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -33,14 +33,14 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=asdict(options) ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert Options(**result["data"]) == options @@ -51,7 +51,7 @@ async def test_invalid_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # enable_subscription must be True if enable_long_press is True (default). diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 273f0e6737d..e3896a436d4 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant, region, brand) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant, region, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -84,7 +84,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, region, brand) -> None: result["flow_id"], CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -105,7 +105,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, region, brand) -> None: "brand": brand[0], }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -126,7 +126,7 @@ async def test_form_auth_timeout(hass: HomeAssistant, region, brand) -> None: "brand": brand[0], }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -147,7 +147,7 @@ async def test_form_generic_auth_exception(hass: HomeAssistant, region, brand) - "brand": brand[0], }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -164,7 +164,7 @@ async def test_form_already_configured(hass: HomeAssistant, region, brand) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -192,7 +192,7 @@ async def test_form_already_configured(hass: HomeAssistant, region, brand) -> No ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -202,7 +202,7 @@ async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -222,7 +222,7 @@ async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_appliances"} @@ -246,7 +246,7 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -274,7 +274,7 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -310,7 +310,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> Non ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( patch( @@ -329,7 +329,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -356,7 +356,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -379,5 +379,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 1d3f1a8c6d2..35e40c4e809 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -31,7 +31,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_full_user_flow( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -71,7 +71,7 @@ async def test_full_flow_with_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_whois.side_effect = throw @@ -80,7 +80,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": reason} @@ -93,7 +93,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -115,7 +115,7 @@ async def test_already_configured( data={CONF_DOMAIN: "HOME-Assistant.io"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 14cb8a03f7a..f7231f56062 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.wiffi.const import DOMAIN from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.core import HomeAssistant @@ -77,7 +77,7 @@ async def test_form(hass: HomeAssistant, dummy_tcp_server) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER @@ -85,7 +85,7 @@ async def test_form(hass: HomeAssistant, dummy_tcp_server) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_addr_in_use(hass: HomeAssistant, addr_in_use) -> None: @@ -98,7 +98,7 @@ async def test_form_addr_in_use(hass: HomeAssistant, addr_in_use) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "addr_in_use" @@ -114,7 +114,7 @@ async def test_form_start_server_failed( result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "start_server_failed" @@ -127,13 +127,13 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMEOUT: 9} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMEOUT] == 9 diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index e3496010c95..ba97f1f7d94 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -59,7 +59,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -75,7 +75,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -87,7 +87,7 @@ async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -101,7 +101,7 @@ async def test_ssdp_not_wilight_abort_3( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -115,7 +115,7 @@ async def test_ssdp_not_supported_abort( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported_device" @@ -140,7 +140,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -163,7 +163,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"WL{WILIGHT_ID}" assert result["data"] diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 9852461f5e2..9f4b265ed4f 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -34,7 +34,7 @@ async def test_full_flow( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={CLIENT_ID}&" @@ -69,7 +69,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Withings" assert "result" in result assert result["result"].unique_id == "600" @@ -100,7 +100,7 @@ async def test_config_non_unique_profile( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={CLIENT_ID}&" @@ -128,7 +128,7 @@ 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["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -151,7 +151,7 @@ async def test_config_reauth_profile( }, data=polling_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -190,7 +190,7 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -213,7 +213,7 @@ async def test_config_reauth_wrong_account( }, data=polling_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -252,7 +252,7 @@ async def test_config_reauth_wrong_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -276,7 +276,7 @@ async def test_config_flow_with_invalid_credentials( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={CLIENT_ID}&" @@ -303,5 +303,5 @@ async def test_config_flow_with_invalid_credentials( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "oauth_error" diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 42b2b8da965..3ade0fb7c3a 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -2,7 +2,7 @@ from datetime import timedelta from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch from urllib.parse import urlparse from aiohttp import ClientConnectionError @@ -14,15 +14,13 @@ from aiowithings import ( ) from freezegun.api import FrozenDateTimeFactory 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 -from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID +from homeassistant.components.withings.const import DOMAIN +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -38,87 +36,6 @@ from tests.components.cloud import mock_cloud from tests.typing import ClientSessionGenerator -def config_schema_validate(withings_config) -> dict: - """Assert a schema config succeeds.""" - hass_config = {DOMAIN: withings_config} - - return CONFIG_SCHEMA(hass_config) - - -def config_schema_assert_fail(withings_config) -> None: - """Assert a schema config will fail.""" - with pytest.raises(vol.MultipleInvalid): - config_schema_validate(withings_config) - - -def test_config_schema_basic_config() -> None: - """Test schema.""" - config_schema_validate( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: True, - } - ) - - -def test_config_schema_client_id() -> None: - """Test schema.""" - config_schema_assert_fail( - {CONF_CLIENT_SECRET: "my_client_secret", CONF_CLIENT_ID: ""} - ) - config_schema_validate( - {CONF_CLIENT_SECRET: "my_client_secret", CONF_CLIENT_ID: "my_client_id"} - ) - - -def test_config_schema_client_secret() -> None: - """Test schema.""" - config_schema_assert_fail({CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: ""}) - config_schema_validate( - {CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: "my_client_secret"} - ) - - -def test_config_schema_use_webhook() -> None: - """Test schema.""" - config_schema_validate( - {CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: "my_client_secret"} - ) - config = config_schema_validate( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: True, - } - ) - assert config[DOMAIN][CONF_USE_WEBHOOK] is True - config = config_schema_validate( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: False, - } - ) - assert config[DOMAIN][CONF_USE_WEBHOOK] is False - config_schema_assert_fail( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: "A", - } - ) - - -async def test_async_setup_no_config(hass: HomeAssistant) -> None: - """Test method.""" - hass.async_create_task = MagicMock() - - await async_setup(hass, {}) - - hass.async_create_task.assert_not_called() - - async def test_data_manager_webhook_subscription( hass: HomeAssistant, withings: AsyncMock, diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 72da4b9d973..8966006e47f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -21,7 +21,7 @@ from . import ( setup_integration, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.freeze_time("2023-10-21") @@ -36,15 +36,10 @@ async def test_all_entities( """Test all entities.""" with patch("homeassistant.components.withings.PLATFORMS", [Platform.SENSOR]): await setup_integration(hass, polling_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, polling_config_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") + await snapshot_platform( + hass, entity_registry, snapshot, polling_config_entry.entry_id + ) async def test_update_failed( diff --git a/tests/components/wiz/test_binary_sensor.py b/tests/components/wiz/test_binary_sensor.py index adfef066e16..d9e8d7170c7 100644 --- a/tests/components/wiz/test_binary_sensor.py +++ b/tests/components/wiz/test_binary_sensor.py @@ -1,8 +1,8 @@ """Tests for WiZ binary_sensor platform.""" -from homeassistant import config_entries from homeassistant.components import wiz from homeassistant.components.wiz.binary_sensor import OCCUPANCY_UNIQUE_ID +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -70,7 +70,7 @@ async def test_binary_sensor_restored_from_registry(hass: HomeAssistant) -> None await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_binary_sensor_never_created_no_error_on_unload( @@ -80,4 +80,4 @@ async def test_binary_sensor_never_created_no_error_on_unload( _, entry = await async_setup_integration(hass) await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index 1b84a048fd2..c60e080f6d4 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.wiz.config_flow import CONF_DEVICE from homeassistant.components.wiz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -49,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # Patch functions with ( @@ -68,7 +69,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "WiZ Dimmable White ABCABC" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -82,7 +83,7 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -91,7 +92,7 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_ip"} with ( @@ -110,7 +111,7 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "WiZ Dimmable White ABCABC" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -145,7 +146,7 @@ async def test_user_form_exceptions( TEST_CONNECTION, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_base} @@ -168,7 +169,7 @@ async def test_form_updates_unique_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -193,7 +194,7 @@ async def test_discovered_by_dhcp_connection_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -270,7 +271,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with ( @@ -291,7 +292,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == name assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -324,7 +325,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_updates_host( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -344,7 +345,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret bulb.getMac = AsyncMock(side_effect=OSError) _, entry = await async_setup_integration(hass, wizlight=bulb) assert entry.data[CONF_HOST] == FAKE_IP - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_wizlight(): @@ -353,9 +354,9 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_setup_via_discovery(hass: HomeAssistant) -> None: @@ -364,7 +365,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -372,7 +373,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -380,7 +381,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -388,7 +389,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -407,7 +408,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "WiZ Dimmable White ABCABC" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -419,7 +420,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -427,7 +428,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -437,7 +438,7 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -445,7 +446,7 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -462,7 +463,7 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -472,7 +473,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass: HomeAssistant) DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -483,7 +484,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass: HomeAssistant) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -501,7 +502,7 @@ async def test_discovery_with_firmware_update(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" # In between discovery and when the user clicks to set it up the firmware @@ -528,7 +529,7 @@ async def test_discovery_with_firmware_update(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "WiZ RGBWW Tunable ABCABC" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -564,7 +565,7 @@ async def test_discovered_during_onboarding(hass: HomeAssistant, source, data) - ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "WiZ Dimmable White ABCABC" assert result["data"] == { CONF_HOST: "1.1.1.1", diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index d6813263fcc..c3438aed1b2 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -3,8 +3,8 @@ import datetime from unittest.mock import AsyncMock, patch -from homeassistant import config_entries from homeassistant.components.wiz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -28,21 +28,21 @@ async def test_setup_retry(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.getMac = AsyncMock(side_effect=OSError) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_discovery(), _patch_wizlight(device=bulb): - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) - await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + await hass.async_block_till_done(wait_background_tasks=True) + assert entry.state is ConfigEntryState.LOADED async def test_cleanup_on_shutdown(hass: HomeAssistant) -> None: """Test the socket is cleaned up on shutdown.""" bulb = _mocked_wizlight(None, None, FAKE_SOCKET) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() bulb.async_close.assert_called_once() @@ -64,7 +64,7 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY bulb.async_close.assert_called_once() @@ -73,14 +73,14 @@ async def test_wrong_device_now_has_our_ip(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.mac = "dddddddddddd" _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_reload_on_title_change(hass: HomeAssistant) -> None: """Test the integration gets reloaded when the title is updated.""" bulb = _mocked_wizlight(None, None, FAKE_SOCKET) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() with _patch_discovery(), _patch_wizlight(device=bulb): diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index fc2a11c3e46..a1529eda1c7 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -25,14 +25,14 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -64,14 +64,14 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("description_placeholders") == {CONF_NAME: "WLED RGB Light"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2.get("title") == "WLED RGB Light" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -101,7 +101,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -120,7 +120,7 @@ async def test_connection_error(hass: HomeAssistant, mock_wled: MagicMock) -> No data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -145,7 +145,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -163,7 +163,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -180,7 +180,7 @@ async def test_user_with_cct_channel_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -205,7 +205,7 @@ async def test_zeroconf_without_mac_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -230,7 +230,7 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -255,7 +255,7 @@ async def test_zeroconf_with_cct_channel_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -267,7 +267,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result2 = await hass.config_entries.options.async_configure( @@ -275,7 +275,7 @@ async def test_options_flow( user_input={CONF_KEEP_MAIN_LIGHT: True}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_KEEP_MAIN_LIGHT: True, } diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index bee646deae8..bd71d9d3180 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -6,7 +6,7 @@ from httpcore import ConnectError from wolf_comm.models import Device from wolf_comm.token_auth import InvalidAuth -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.wolflink.const import ( DEVICE_GATEWAY, DEVICE_ID, @@ -15,6 +15,7 @@ from homeassistant.components.wolflink.const import ( ) 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 @@ -40,7 +41,7 @@ async def test_show_form(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -54,7 +55,7 @@ async def test_device_step_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device" @@ -76,7 +77,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result_create_entry["type"] is FlowResultType.CREATE_ENTRY assert result_create_entry["title"] == CONFIG[DEVICE_NAME] assert result_create_entry["data"] == CONFIG @@ -91,7 +92,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -105,7 +106,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -119,7 +120,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -145,5 +146,5 @@ async def test_already_configured_error(hass: HomeAssistant) -> None: {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.FlowResultType.ABORT + assert result_create_entry["type"] is FlowResultType.ABORT assert result_create_entry["reason"] == "already_configured" diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index 75677143ecb..7eb3065e576 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -58,7 +58,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -78,7 +78,7 @@ async def test_form_no_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -99,7 +99,7 @@ async def test_form_no_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -117,7 +117,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -139,7 +139,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -185,7 +185,7 @@ async def test_options_form(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Workday Sensor", "country": "DE", @@ -205,7 +205,7 @@ async def test_form_incorrect_dates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -257,7 +257,7 @@ async def test_form_incorrect_dates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -333,7 +333,7 @@ async def test_options_form_incorrect_dates(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Workday Sensor", "country": "DE", @@ -392,7 +392,7 @@ async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -402,7 +402,7 @@ async def test_form_incorrect_date_range(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -454,7 +454,7 @@ async def test_form_incorrect_date_range(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -530,7 +530,7 @@ async def test_options_form_incorrect_date_ranges(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Workday Sensor", "country": "DE", @@ -563,7 +563,7 @@ async def test_language( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -586,7 +586,7 @@ async def test_language( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", diff --git a/tests/components/workday/test_init.py b/tests/components/workday/test_init.py index e9de315e1d1..1e0c9cbebc6 100644 --- a/tests/components/workday/test_init.py +++ b/tests/components/workday/test_init.py @@ -6,7 +6,7 @@ from datetime import datetime from freezegun.api import FrozenDateTimeFactory -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.util.dt import UTC @@ -35,7 +35,7 @@ async def test_update_options( freezer.move_to(datetime(2023, 4, 12, 12, tzinfo=UTC)) # Monday entry = await init_integration(hass, TEST_CONFIG_WITH_PROVINCE) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.update_listeners is not None state = hass.states.get("binary_sensor.workday_sensor") assert state.state == "on" @@ -47,6 +47,6 @@ async def test_update_options( await hass.async_block_till_done() entry_check = hass.config_entries.async_get_entry("1") - assert entry_check.state == config_entries.ConfigEntryState.LOADED + assert entry_check.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.workday_sensor") assert state.state == "off" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index 19a329cb913..3fe8499ac97 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ws66i.const import ( CONF_SOURCE_1, CONF_SOURCE_2, @@ -16,6 +16,7 @@ from homeassistant.components.ws66i.const import ( ) from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .test_media_player import AttrDict @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: ws66i_instance.open.assert_called_once() ws66i_instance.close.assert_called_once() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "WS66i Amp" assert result2["data"] == {CONF_IP_ADDRESS: CONFIG[CONF_IP_ADDRESS]} @@ -71,7 +72,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -88,7 +89,7 @@ async def test_form_wrong_ip(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -105,7 +106,7 @@ async def test_generic_exception(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -130,7 +131,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -145,7 +146,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == { "1": "one", "2": "too", diff --git a/tests/components/wyoming/test_config_flow.py b/tests/components/wyoming/test_config_flow.py index c15eb81a1e2..e363a0650bc 100644 --- a/tests/components/wyoming/test_config_flow.py +++ b/tests/components/wyoming/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form_stt(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -62,7 +62,7 @@ async def test_form_stt(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test ASR" assert result2["data"] == { "host": "1.1.1.1", @@ -76,7 +76,7 @@ async def test_form_tts(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -92,7 +92,7 @@ async def test_form_tts(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test TTS" assert result2["data"] == { "host": "1.1.1.1", @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -141,7 +141,7 @@ async def test_no_supported_services(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_services" @@ -159,7 +159,7 @@ async def test_hassio_addon_discovery( context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "Piper"} @@ -169,7 +169,7 @@ async def test_hassio_addon_discovery( ) as mock_wyoming: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +189,7 @@ async def test_hassio_addon_already_configured(hass: HomeAssistant) -> None: data=ADDON_DISCOVERY, context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -207,7 +207,7 @@ async def test_hassio_addon_cannot_connect(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "cannot_connect"} @@ -225,7 +225,7 @@ async def test_hassio_addon_no_supported_services(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "no_services" @@ -245,14 +245,14 @@ async def test_zeroconf_discovery( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "zeroconf_confirm" assert result.get("description_placeholders") == { "name": SATELLITE_INFO.satellite.name } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -275,7 +275,7 @@ async def test_zeroconf_discovery_no_port( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_port" @@ -295,5 +295,5 @@ async def test_zeroconf_discovery_no_services( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_services" diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index d4f7a697839..e547909f946 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -3,13 +3,14 @@ from http import HTTPStatus from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.xbox.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -27,7 +28,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "xbox", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -54,7 +55,7 @@ async def test_full_flow( }, ) - scope = "+".join(["Xboxlive.signin", "Xboxlive.offline_access"]) + scope = "Xboxlive.signin+Xboxlive.offline_access" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 0a576b70bdf..1b1d898add1 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -48,7 +48,7 @@ def mocked_requests(*args, **kwargs): raise requests.HTTPError(self.status_code) data = kwargs.get("data") - global FIRST_CALL + global FIRST_CALL # noqa: PLW0603 if data and data.get("username", None) == INVALID_USERNAME: # deliver an invalid token diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 67991714203..141e245815e 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" @@ -89,7 +90,7 @@ async def test_config_flow_user_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -98,7 +99,7 @@ async def test_config_flow_user_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -107,7 +108,7 @@ async def test_config_flow_user_success(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -126,7 +127,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -141,7 +142,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["errors"] == {} @@ -150,7 +151,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: {"select_ip": TEST_HOST_2}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -159,7 +160,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST_2, @@ -178,7 +179,7 @@ async def test_config_flow_user_no_key_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -187,7 +188,7 @@ async def test_config_flow_user_no_key_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -196,7 +197,7 @@ async def test_config_flow_user_no_key_success(hass: HomeAssistant) -> None: {CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -215,7 +216,7 @@ async def test_config_flow_user_host_mac_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -234,7 +235,7 @@ async def test_config_flow_user_host_mac_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -243,7 +244,7 @@ async def test_config_flow_user_host_mac_success(hass: HomeAssistant) -> None: {CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -262,7 +263,7 @@ async def test_config_flow_user_discovery_error(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -277,7 +278,7 @@ async def test_config_flow_user_discovery_error(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} @@ -288,7 +289,7 @@ async def test_config_flow_user_invalid_interface(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -303,7 +304,7 @@ async def test_config_flow_user_invalid_interface(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"} @@ -314,7 +315,7 @@ async def test_config_flow_user_invalid_host(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -333,7 +334,7 @@ async def test_config_flow_user_invalid_host(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"host": "invalid_host"} @@ -344,7 +345,7 @@ async def test_config_flow_user_invalid_mac(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -363,7 +364,7 @@ async def test_config_flow_user_invalid_mac(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"mac": "invalid_mac"} @@ -374,7 +375,7 @@ async def test_config_flow_user_invalid_key(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -389,7 +390,7 @@ async def test_config_flow_user_invalid_key(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -398,7 +399,7 @@ async def test_config_flow_user_invalid_key(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {const.CONF_KEY: "invalid_key"} @@ -419,7 +420,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -428,7 +429,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -437,7 +438,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -466,7 +467,7 @@ async def test_zeroconf_missing_data(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_aqara" @@ -486,5 +487,5 @@ async def test_zeroconf_unknown_device(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_aqara" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 8b3ff2ef4ab..b61615e0f79 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -30,7 +30,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -57,7 +57,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_slow" with patch( @@ -66,7 +66,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -96,7 +96,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" with patch( @@ -107,7 +107,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full( user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -129,7 +129,7 @@ async def test_async_step_bluetooth_during_onboarding(hass: HomeAssistant) -> No data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result["data"] == {} assert result["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -146,7 +146,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption( context={"source": config_entries.SOURCE_BLUETOOTH}, data=YLKG07YL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" with patch( @@ -156,7 +156,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -171,14 +171,14 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key( context={"source": config_entries.SOURCE_BLUETOOTH}, data=YLKG07YL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -190,7 +190,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -205,14 +205,14 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len context={"source": config_entries.SOURCE_BLUETOOTH}, data=YLKG07YL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "expected_24_characters" @@ -224,7 +224,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -239,7 +239,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption( context={"source": config_entries.SOURCE_BLUETOOTH}, data=JTYJGD03MI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" with patch( @@ -250,7 +250,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -265,7 +265,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( context={"source": config_entries.SOURCE_BLUETOOTH}, data=JTYJGD03MI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" result2 = await hass.config_entries.flow.async_configure( @@ -273,7 +273,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -286,7 +286,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -301,7 +301,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( context={"source": config_entries.SOURCE_BLUETOOTH}, data=JTYJGD03MI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" result2 = await hass.config_entries.flow.async_configure( @@ -309,7 +309,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( user_input={"bindkey": "5b51a7c91cde6707c9ef18fda143a58"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -322,7 +322,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -335,7 +335,7 @@ async def test_async_step_bluetooth_not_xiaomi(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSOR_PUSH_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -345,7 +345,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -362,7 +362,7 @@ async def test_async_step_user_no_devices_found_2(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -376,7 +376,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True @@ -385,7 +385,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "58:2D:34:35:93:21"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Temperature/Humidity Sensor 9321 (LYWSDCGQ)" assert result2["data"] == {} assert result2["result"].unique_id == "58:2D:34:35:93:21" @@ -401,7 +401,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", @@ -411,7 +411,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "confirm_slow" with patch( @@ -420,7 +420,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result3["data"] == {} assert result3["result"].unique_id == "A4:C1:38:56:53:84" @@ -436,7 +436,7 @@ async def test_async_step_user_short_payload_then_full(hass: HomeAssistant) -> N DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" async def _async_process_advertisements( @@ -457,7 +457,7 @@ async def test_async_step_user_short_payload_then_full(hass: HomeAssistant) -> N result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" with patch( @@ -468,7 +468,7 @@ async def test_async_step_user_short_payload_then_full(hass: HomeAssistant) -> N user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} @@ -485,14 +485,14 @@ async def test_async_step_user_with_found_devices_v4_encryption( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:EF:44:E3:9C:BC"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" with patch( @@ -503,7 +503,7 @@ async def test_async_step_user_with_found_devices_v4_encryption( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -522,7 +522,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Pick a device @@ -530,7 +530,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( result["flow_id"], user_input={"address": "54:EF:44:E3:9C:BC"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" # Try an incorrect key @@ -538,7 +538,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -551,7 +551,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -570,7 +570,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Select a single device @@ -578,7 +578,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length result["flow_id"], user_input={"address": "54:EF:44:E3:9C:BC"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" # Try an incorrect key @@ -587,8 +587,8 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length user_input={"bindkey": "5b51a7c91cde6707c9ef1dfda143a58"}, ) - assert result2["type"] == FlowResultType.FORM - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -601,7 +601,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -619,14 +619,14 @@ async def test_async_step_user_with_found_devices_legacy_encryption( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "F8:24:41:C5:98:8B"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_legacy" with patch( @@ -636,7 +636,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -654,14 +654,14 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "F8:24:41:C5:98:8B"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_legacy" # Enter an incorrect code @@ -669,7 +669,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -681,7 +681,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -699,14 +699,14 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "F8:24:41:C5:98:8B"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_legacy" # Enter an incorrect code @@ -714,7 +714,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le result["flow_id"], user_input={"bindkey": "b85307518487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "expected_24_characters" @@ -726,7 +726,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -742,7 +742,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -758,7 +758,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "58:2D:34:35:93:21"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -780,7 +780,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -797,7 +797,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -808,7 +808,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -816,7 +816,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -829,7 +829,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -840,7 +840,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True @@ -849,7 +849,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "00:81:F9:DD:6F:C1"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -903,7 +903,7 @@ async def test_async_step_reauth_legacy(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -952,7 +952,7 @@ async def test_async_step_reauth_legacy_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -960,7 +960,7 @@ async def test_async_step_reauth_legacy_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1009,7 +1009,7 @@ async def test_async_step_reauth_v4(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1058,7 +1058,7 @@ async def test_async_step_reauth_v4_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -1066,7 +1066,7 @@ async def test_async_step_reauth_v4_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1094,5 +1094,5 @@ async def test_async_step_reauth_abort_early(hass: HomeAssistant) -> None: data=entry.data | {"device": device}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 7645f67732e..481be189ddd 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -8,11 +8,12 @@ from micloud.micloudexception import MiCloudAccessDenied from miio import DeviceException import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_MAC @@ -118,7 +119,7 @@ async def test_config_flow_step_gateway_connect_error(hass: HomeAssistant) -> No const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -127,7 +128,7 @@ async def test_config_flow_step_gateway_connect_error(hass: HomeAssistant) -> No {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -140,7 +141,7 @@ async def test_config_flow_step_gateway_connect_error(hass: HomeAssistant) -> No {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -151,7 +152,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -160,7 +161,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -169,7 +170,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_MODEL assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -189,7 +190,7 @@ async def test_config_flow_gateway_cloud_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -202,7 +203,7 @@ async def test_config_flow_gateway_cloud_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -222,7 +223,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -239,7 +240,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["errors"] == {} @@ -248,7 +249,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - {"select_device": f"{TEST_NAME2} - {TEST_MODEL}"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME2 assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -268,7 +269,7 @@ async def test_config_flow_gateway_cloud_incomplete(hass: HomeAssistant) -> None const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -280,7 +281,7 @@ async def test_config_flow_gateway_cloud_incomplete(hass: HomeAssistant) -> None }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_credentials_incomplete"} @@ -291,7 +292,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -308,7 +309,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_login_error"} @@ -325,7 +326,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_login_error"} @@ -342,7 +343,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -352,7 +353,7 @@ async def test_config_flow_gateway_cloud_no_devices(hass: HomeAssistant) -> None const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -369,7 +370,7 @@ async def test_config_flow_gateway_cloud_no_devices(hass: HomeAssistant) -> None }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_no_devices"} @@ -386,7 +387,7 @@ async def test_config_flow_gateway_cloud_no_devices(hass: HomeAssistant) -> None }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -396,7 +397,7 @@ async def test_config_flow_gateway_cloud_missing_token(hass: HomeAssistant) -> N const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -424,7 +425,7 @@ async def test_config_flow_gateway_cloud_missing_token(hass: HomeAssistant) -> N }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_info" @@ -444,7 +445,7 @@ async def test_zeroconf_gateway_success(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -457,7 +458,7 @@ async def test_zeroconf_gateway_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -487,7 +488,7 @@ async def test_zeroconf_unknown_device(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_miio" @@ -507,7 +508,7 @@ async def test_zeroconf_no_data(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_miio" @@ -527,7 +528,7 @@ async def test_zeroconf_missing_data(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_miio" @@ -537,7 +538,7 @@ async def test_config_flow_step_device_connect_error(hass: HomeAssistant) -> Non const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -546,7 +547,7 @@ async def test_config_flow_step_device_connect_error(hass: HomeAssistant) -> Non {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -559,7 +560,7 @@ async def test_config_flow_step_device_connect_error(hass: HomeAssistant) -> Non {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -570,7 +571,7 @@ async def test_config_flow_step_unknown_device(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -579,7 +580,7 @@ async def test_config_flow_step_unknown_device(hass: HomeAssistant) -> None: {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -594,7 +595,7 @@ async def test_config_flow_step_unknown_device(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "unknown_device"} @@ -605,7 +606,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -614,7 +615,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -627,7 +628,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -640,7 +641,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - {CONF_MODEL: TEST_MODEL}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -650,7 +651,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -659,7 +660,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -674,7 +675,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "wrong_token"} @@ -689,7 +690,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) {CONF_MODEL: overwrite_model}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == overwrite_model assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -709,7 +710,7 @@ async def config_flow_device_success(hass, model_to_test): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -718,7 +719,7 @@ async def config_flow_device_success(hass, model_to_test): {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -733,7 +734,7 @@ async def config_flow_device_success(hass, model_to_test): {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -755,7 +756,7 @@ async def config_flow_generic_roborock(hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -764,7 +765,7 @@ async def config_flow_generic_roborock(hass): {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -779,7 +780,7 @@ async def config_flow_generic_roborock(hass): {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == dummy_model assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -809,7 +810,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -818,7 +819,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -833,7 +834,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): {CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -897,7 +898,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -907,7 +908,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_CLOUD_SUBDEVICES: True, } @@ -937,7 +938,7 @@ async def test_options_flow_incomplete(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -946,7 +947,7 @@ async def test_options_flow_incomplete(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cloud_credentials_incomplete"} @@ -979,7 +980,7 @@ async def test_reauth(hass: HomeAssistant) -> None: data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -987,7 +988,7 @@ async def test_reauth(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -1000,7 +1001,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" config_data = config_entry.data.copy() diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 5eed34a2423..4ef201d2122 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -85,7 +85,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -107,7 +107,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -142,7 +142,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -163,7 +163,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -226,7 +226,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -248,7 +248,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -288,7 +288,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -296,5 +296,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"lock_code_digits": 6}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"lock_code_digits": 6} diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index 34ffc55ac3f..15552fdec5f 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -57,7 +57,7 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -80,7 +80,7 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -101,7 +101,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -125,7 +125,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -150,7 +150,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: CONF_SLOT: 66, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {CONF_KEY: "invalid_key_format"} @@ -162,7 +162,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: CONF_SLOT: 66, }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "user" assert result3["errors"] == {CONF_KEY: "invalid_key_format"} @@ -174,7 +174,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: CONF_SLOT: 999, }, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "user" assert result4["errors"] == {CONF_SLOT: "invalid_key_index"} @@ -197,7 +197,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result5["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -218,7 +218,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -236,7 +236,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -259,7 +259,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -280,7 +280,7 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -298,7 +298,7 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {CONF_KEY: "invalid_auth"} @@ -321,7 +321,7 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -342,7 +342,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -360,7 +360,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -383,7 +383,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -402,7 +402,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=YALE_ACCESS_LOCK_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -425,7 +425,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -454,7 +454,7 @@ async def test_integration_discovery_success(hass: HomeAssistant) -> None: "serial": "M1XXX012LU", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -468,7 +468,7 @@ async def test_integration_discovery_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Front Door" assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -497,7 +497,7 @@ async def test_integration_discovery_device_not_found(hass: HomeAssistant) -> No "serial": "M1XXX012LU", }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -510,7 +510,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( context={"source": config_entries.SOURCE_BLUETOOTH}, data=YALE_ACCESS_LOCK_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} flows = [ @@ -538,7 +538,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -563,7 +563,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Front Door" assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -619,7 +619,7 @@ async def test_integration_discovery_updates_key_unique_local_name( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" assert entry.data[CONF_SLOT] == 66 @@ -658,7 +658,7 @@ async def test_integration_discovery_updates_key_without_unique_local_name( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" assert entry.data[CONF_SLOT] == 66 @@ -707,7 +707,7 @@ async def test_integration_discovery_updates_key_duplicate_local_name( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" assert entry.data[CONF_SLOT] == 66 @@ -725,7 +725,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres context={"source": config_entries.SOURCE_BLUETOOTH}, data=LOCK_DISCOVERY_INFO_UUID_ADDRESS, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} flows = [ @@ -753,7 +753,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -778,7 +778,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Front Door" assert result2["data"] == { CONF_LOCAL_NAME: LOCK_DISCOVERY_INFO_UUID_ADDRESS.name, @@ -805,7 +805,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ context={"source": config_entries.SOURCE_BLUETOOTH}, data=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} flows = [ @@ -833,7 +833,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -863,7 +863,7 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -912,13 +912,13 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( }, ) await hass.async_block_till_done() - assert discovery_result["type"] == FlowResultType.ABORT + assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_in_progress" user_flow_event.set() user_flow_result = await user_flow_task - assert user_flow_result["type"] == FlowResultType.CREATE_ENTRY + assert user_flow_result["type"] is FlowResultType.CREATE_ENTRY assert user_flow_result["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert user_flow_result["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -950,7 +950,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_validate" with patch( @@ -966,7 +966,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_validate" assert result2["errors"] == {"base": "no_longer_in_range"} @@ -992,7 +992,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -1022,7 +1022,7 @@ async def test_options(hass: HomeAssistant) -> None: entry.entry_id, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_options" with patch( @@ -1037,6 +1037,6 @@ async def test_options(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert entry.options == {CONF_ALWAYS_CONNECTED: True} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 9740bd70a87..1c51b315a5a 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import patch from aiomusiccast import MusicCastConnectionException import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.yamaha_musiccast.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -135,13 +136,13 @@ async def test_user_input_device_not_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "none"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -153,13 +154,13 @@ async def test_user_input_non_yamaha_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_musiccast_device"} @@ -183,7 +184,7 @@ async def test_user_input_device_already_existing( {"host": "192.168.188.18"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -195,13 +196,13 @@ async def test_user_input_unknown_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -216,13 +217,13 @@ async def test_user_input_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -242,13 +243,13 @@ async def test_user_input_device_found_no_ssdp( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -278,7 +279,7 @@ async def test_ssdp_discovery_failed( ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "yxc_control_url_missing" @@ -300,7 +301,7 @@ async def test_ssdp_discovery_successful_add_device( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -309,7 +310,7 @@ async def test_ssdp_discovery_successful_add_device( {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -341,7 +342,7 @@ async def test_ssdp_discovery_existing_device_update( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data[CONF_HOST] == "127.0.0.1" assert mock_entry.data["upnp_description"] == "http://127.0.0.1/desc.xml" diff --git a/tests/components/yandex_transport/test_sensor.py b/tests/components/yandex_transport/test_sensor.py index d302ce17a26..5ad9fa92c39 100644 --- a/tests/components/yandex_transport/test_sensor.py +++ b/tests/components/yandex_transport/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch import pytest -import homeassistant.components.sensor as sensor +from homeassistant.components import sensor from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/yardian/test_config_flow.py b/tests/components/yardian/test_config_flow.py index c93f48d1c48..1630286733f 100644 --- a/tests/components/yardian/test_config_flow.py +++ b/tests/components/yardian/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == PRODUCT_NAME assert result2["data"] == { "host": "fake_host", @@ -65,7 +65,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} # Should be recoverable after hits error @@ -82,7 +82,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == PRODUCT_NAME assert result3["data"] == { "host": "fake_host", @@ -113,7 +113,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} # Should be recoverable after hits error @@ -130,7 +130,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == PRODUCT_NAME assert result3["data"] == { "host": "fake_host", @@ -161,7 +161,7 @@ async def test_form_uncategorized_error( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} # Should be recoverable after hits error @@ -178,7 +178,7 @@ async def test_form_uncategorized_error( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == PRODUCT_NAME assert result3["data"] == { "host": "fake_host", diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 41d60c8652a..4d788ba8258 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -67,12 +67,12 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -80,12 +80,12 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -98,7 +98,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == UNIQUE_FRIENDLY_NAME assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS, CONF_MODEL: MODEL} await hass.async_block_till_done() @@ -109,13 +109,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -142,7 +142,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -151,7 +151,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No await hass.async_block_till_done() await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -160,13 +160,13 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -178,7 +178,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == UNIQUE_FRIENDLY_NAME assert result3["data"] == { CONF_ID: ID, @@ -192,13 +192,13 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +215,7 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -241,7 +241,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # Success @@ -255,7 +255,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_NAME: DEFAULT_NAME, @@ -278,7 +278,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -287,7 +287,7 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -302,7 +302,7 @@ async def test_manual(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -331,7 +331,7 @@ async def test_manual(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Color 0x15243f" assert result4["data"] == { CONF_HOST: IP_ADDRESS, @@ -353,7 +353,7 @@ async def test_manual(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -382,7 +382,7 @@ async def test_options(hass: HomeAssistant) -> None: assert hass.states.get(f"light.{NAME}_nightlight") is None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" config[CONF_NIGHTLIGHT_SWITCH] = True @@ -394,7 +394,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == config assert result2["data"] == config_entry.options assert hass.states.get(f"light.{NAME}_nightlight") is not None @@ -425,7 +425,7 @@ async def test_options_unknown_model(hass: HomeAssistant) -> None: assert hass.states.get(f"light.{NAME}_nightlight") is None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" config[CONF_NIGHTLIGHT_SWITCH] = True @@ -436,7 +436,7 @@ async def test_options_unknown_model(hass: HomeAssistant) -> None: result["flow_id"], user_input ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == config assert result2["data"] == config_entry.options assert hass.states.get(f"light.{NAME}_nightlight") is not None @@ -447,7 +447,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -469,7 +469,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: None, @@ -500,7 +500,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -516,7 +516,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with ( @@ -532,7 +532,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with ( @@ -549,7 +549,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -590,7 +590,7 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -604,7 +604,7 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", @@ -623,7 +623,7 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -665,7 +665,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -683,7 +683,7 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -697,7 +697,7 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", @@ -717,7 +717,7 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -737,7 +737,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -751,7 +751,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", @@ -773,7 +773,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_bulb = _mocked_bulb() @@ -789,7 +789,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -813,7 +813,7 @@ async def test_discovery_updates_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -844,7 +844,7 @@ async def test_discovery_updates_ip_no_reload_setup_in_progress( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS assert len(mock_setup_entry.mock_calls) == 0 @@ -868,7 +868,7 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -916,7 +916,7 @@ async def test_discovered_during_onboarding(hass: HomeAssistant, source, data) - ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index af442d1c8d0..0bff635fb6e 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -69,7 +69,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY 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) # The discovery should update the ip address assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -78,7 +78,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): 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 config_entry.state is ConfigEntryState.LOADED binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( @@ -362,7 +362,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant) -> None: _patch_discovery_interval(), ): 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) assert config_entry.state is ConfigEntryState.LOADED @@ -380,7 +380,7 @@ async def test_async_listen_error_late_discovery( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() 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 config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() @@ -388,7 +388,7 @@ async def test_async_listen_error_late_discovery( with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): 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 config_entry.state is ConfigEntryState.LOADED assert config_entry.data[CONF_DETECTED_MODEL] == MODEL @@ -411,7 +411,7 @@ async def test_fail_to_fetch_initial_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() 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 config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() @@ -419,7 +419,7 @@ async def test_fail_to_fetch_initial_state( with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): 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 config_entry.state is ConfigEntryState.LOADED @@ -502,7 +502,7 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.data[CONF_ID] == ID 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) with ( _patch_discovery(), @@ -511,7 +511,7 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: 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() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -535,7 +535,7 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.unique_id == ID 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) with ( _patch_discovery(), @@ -544,7 +544,7 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: 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() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index f7abda0bc4b..f62bd3ac1ac 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -5,9 +5,11 @@ from unittest.mock import patch from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components import application_credentials +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -24,7 +26,7 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -34,7 +36,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -65,7 +67,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -107,7 +109,7 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -136,7 +138,7 @@ async def test_abort_if_authorization_timeout( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -217,6 +219,6 @@ async def test_reauthentication( assert token_data["refresh_token"] == "mock-refresh-token" assert token_data["type"] == "Bearer" assert token_data["expires_in"] == 60 - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index bc53c55539b..90f17e04efb 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -26,7 +26,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -42,7 +42,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "localhost" assert len(mocked_youless.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_not_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -67,5 +67,5 @@ async def test_not_found(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert len(mocked_youless.mock_calls) == 1 diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index c8857626384..95a56155980 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -65,7 +65,7 @@ async def test_full_flow( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "channels" result = await hass.config_entries.flow.async_configure( @@ -75,7 +75,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert "result" in result assert result["result"].unique_id == "UC_x5XG1OV2P6uZZ5FSM9Ttw" @@ -122,7 +122,7 @@ async def test_flow_abort_without_channel( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_channel" @@ -163,7 +163,7 @@ async def test_flow_abort_without_subscriptions( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_subscriptions" @@ -203,7 +203,7 @@ async def test_flow_http_error( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "access_not_configured" assert result["description_placeholders"]["message"] == ( "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." @@ -300,7 +300,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason assert result["description_placeholders"] == placeholders assert len(mock_setup.mock_calls) == calls @@ -345,7 +345,7 @@ async def test_flow_exception( "homeassistant.components.youtube.config_flow.YouTube", side_effect=Exception ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -362,7 +362,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -371,5 +371,5 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]} diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index bc95afa7936..f67eda67a49 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -23,14 +23,14 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM LOGGER.debug(result) assert result.get("data_schema") != "" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_STATION_ID] == TEST_STATION_ID assert "result" in result @@ -48,7 +48,7 @@ async def test_error_closest_station( DOMAIN, context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -63,7 +63,7 @@ async def test_error_update( context={"source": SOURCE_USER}, ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM LOGGER.debug(result) assert result.get("data_schema") != "" mock_zamg.update.side_effect = ZamgApiError @@ -71,7 +71,7 @@ async def test_error_update( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -87,12 +87,12 @@ async def test_user_flow_duplicate( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_STATION_ID] == TEST_STATION_ID assert "result" in result @@ -103,10 +103,10 @@ async def test_user_flow_duplicate( context={"source": SOURCE_USER}, ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index e512b2a668e..33bcb812b63 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -7,6 +7,7 @@ import pyzerproc from homeassistant import config_entries from homeassistant.components.zerproc.config_flow import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_success(hass: HomeAssistant) -> None: @@ -15,7 +16,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -34,7 +35,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Zerproc" assert result2["data"] == {} @@ -47,7 +48,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -65,7 +66,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: {}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 @@ -77,7 +78,7 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -95,7 +96,7 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: {}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/zeversolar/test_config_flow.py b/tests/components/zeversolar/test_config_flow.py index 0bfa5ad547d..53d72f743cb 100644 --- a/tests/components/zeversolar/test_config_flow.py +++ b/tests/components/zeversolar/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) @@ -71,7 +71,7 @@ async def test_form_errors( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == errors await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) @@ -90,7 +90,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") is None assert "flow_id" in result @@ -110,7 +110,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -134,7 +134,7 @@ async def _set_up_zeversolar(hass: HomeAssistant, flow_id: str) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Zeversolar" assert result2["data"] == { CONF_HOST: "test_ip", diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 63d3e9cf747..addf1e24ea9 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -203,7 +203,7 @@ async def async_test_rejoin(hass, zigpy_device, clusters, report_counts, ep_id=1 zha_gateway = get_zha_gateway(hass) await zha_gateway.async_device_initialized(zigpy_device) await hass.async_block_till_done() - for cluster, reports in zip(clusters, report_counts): + for cluster, reports in zip(clusters, report_counts, strict=False): assert cluster.bind.call_count == 1 assert cluster.bind.await_count == 1 if reports: @@ -244,10 +244,7 @@ def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): def new_get_config(config_entry, section, config_key, default): if (section, config_key) in overrides: return overrides[section, config_key] - else: - return async_get_zha_config_value( - config_entry, section, config_key, default - ) + return async_get_zha_config_value(config_entry, section, config_key, default) return patch( f"homeassistant.components.zha.{component}.async_get_zha_config_value", diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b1ac22d544d..7d3722b5037 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -422,12 +422,11 @@ def zha_device_mock( zigpy_device = zigpy_device_mock( endpoints, ieee, manufacturer, model, node_desc, patch_cluster=patch_cluster ) - zha_device = zha_core_device.ZHADevice( + return zha_core_device.ZHADevice( hass, zigpy_device, ZHAGateway(hass, {}, config_entry), ) - return zha_device return _zha_device @@ -546,6 +545,5 @@ def core_rs(hass_storage): } ], } - return return _storage diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 18065420e58..8d3bd76ef61 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, call, patch, sentinel import pytest -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.security as security +from zigpy.profiles import zha +from zigpy.zcl.clusters import security import zigpy.zcl.foundation as zcl_f from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 18e78ae7e57..bd9262a41ce 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -4,9 +4,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.measurement as measurement -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import general, measurement, security from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 4c0c6845885..97aaf2bd871 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -15,13 +15,12 @@ from zhaquirks.const import ( from zhaquirks.tuya.ts0601_valve import ParksideTuyaValveManufCluster from zigpy.const import SIG_EP_PROFILE from zigpy.exceptions import ZigbeeException -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonDeviceClass diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 60c958f20fe..ca21b74e106 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -19,7 +19,7 @@ import zigpy.zcl.clusters from zigpy.zcl.clusters import CLUSTERS_BY_ID import zigpy.zdo.types as zdo_t -import homeassistant.components.zha.core.cluster_handlers as cluster_handlers +from homeassistant.components.zha.core import cluster_handlers, registries from homeassistant.components.zha.core.cluster_handlers.lighting import ( ColorClusterHandler, ) @@ -27,7 +27,6 @@ import homeassistant.components.zha.core.const as zha_const from homeassistant.components.zha.core.device import ZHADevice from homeassistant.components.zha.core.endpoint import Endpoint from homeassistant.components.zha.core.helpers import get_zha_gateway -import homeassistant.components.zha.core.registries as registries from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -120,8 +119,7 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): "test model", ) - zha_device = await zha_device_restored(zigpy_dev) - return zha_device + return await zha_device_restored(zigpy_dev) @pytest.mark.parametrize( diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index bbfca1b1a13..0c8414f458f 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -35,6 +35,7 @@ from homeassistant.config_entries import ( SOURCE_USB, SOURCE_USER, SOURCE_ZEROCONF, + ConfigEntryState, ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant @@ -169,7 +170,7 @@ async def test_zeroconf_discovery_znp(hass: HomeAssistant) -> None: result1["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" result3 = await hass.config_entries.flow.async_configure( @@ -178,7 +179,7 @@ async def test_zeroconf_discovery_znp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "socket://192.168.1.200:6638" assert result3["data"] == { CONF_DEVICE: { @@ -226,7 +227,7 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass: HomeAssistant) -> Non result1["flow_id"], user_input={} ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "choose_formation_strategy" result4 = await hass.config_entries.flow.async_configure( @@ -235,7 +236,7 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "socket://192.168.1.200:1234" assert result4["data"] == { CONF_DEVICE: { @@ -276,7 +277,7 @@ async def test_efr32_via_zeroconf(hass: HomeAssistant) -> None: result1["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" result3 = await hass.config_entries.flow.async_configure( @@ -285,7 +286,7 @@ async def test_efr32_via_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "socket://192.168.1.200:1234" assert result3["data"] == { CONF_DEVICE: { @@ -321,7 +322,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(hass: HomeAssistant) -> DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -355,7 +356,7 @@ async def test_discovery_confirm_final_abort_if_entries(hass: HomeAssistant) -> ) # Config will fail - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -375,7 +376,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -383,7 +384,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -393,7 +394,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "zigbee radio" assert result3["data"] == { "device": { @@ -420,7 +421,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -433,7 +434,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "choose_formation_strategy" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -443,7 +444,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "zigate radio" assert result4["data"] == { "device": { @@ -473,7 +474,7 @@ async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -482,7 +483,7 @@ async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "usb_probe_failed" @@ -507,7 +508,7 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -541,7 +542,7 @@ async def test_discovery_via_usb_path_does_not_change(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyUSB1", @@ -580,7 +581,7 @@ async def test_discovery_via_usb_deconz_already_discovered(hass: HomeAssistant) ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zha_device" @@ -602,7 +603,7 @@ async def test_discovery_via_usb_deconz_already_setup(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zha_device" @@ -626,7 +627,7 @@ async def test_discovery_via_usb_deconz_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -654,7 +655,7 @@ async def test_discovery_via_usb_zha_ignored_updates(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -684,7 +685,7 @@ async def test_discovery_already_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -706,7 +707,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: zigpy.config.CONF_DEVICE_PATH: port_select, }, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_formation_strategy" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -716,7 +717,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"].startswith(port.description) assert result2["data"] == { "device": { @@ -749,7 +750,7 @@ async def test_user_flow_not_detected(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_pick_radio_type" @@ -761,7 +762,7 @@ async def test_user_flow_show_form(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" @@ -773,7 +774,7 @@ async def test_user_flow_show_manual(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_pick_radio_type" @@ -785,7 +786,7 @@ async def test_user_flow_manual(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_pick_radio_type" @@ -798,7 +799,7 @@ async def test_pick_radio_flow(hass: HomeAssistant, radio_type) -> None: context={CONF_SOURCE: "manual_pick_radio_type"}, data={CONF_RADIO_TYPE: radio_type}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_port_config" @@ -812,7 +813,7 @@ async def test_user_flow_existing_config_entry(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT @patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) @@ -885,7 +886,7 @@ async def test_user_port_config_fail(probe_mock, hass: HomeAssistant) -> None: result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_port_config" assert result["errors"]["base"] == "cannot_connect" assert probe_mock.await_count == 1 @@ -907,7 +908,7 @@ async def test_user_port_config(probe_mock, hass: HomeAssistant) -> None: user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_formation_strategy" result2 = await hass.config_entries.flow.async_configure( @@ -946,7 +947,7 @@ async def test_hardware(onboarded, hass: HomeAssistant) -> None: if onboarded: # Confirm discovery - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -957,7 +958,7 @@ async def test_hardware(onboarded, hass: HomeAssistant) -> None: # No need to confirm result2 = result1 - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" result3 = await hass.config_entries.flow.async_configure( @@ -997,7 +998,7 @@ async def test_hardware_already_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_HARDWARE}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -1011,7 +1012,7 @@ async def test_hardware_invalid_data(hass: HomeAssistant, data) -> None: DOMAIN, context={"source": config_entries.SOURCE_HARDWARE}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_hardware_data" @@ -1056,7 +1057,7 @@ def pick_radio(hass): }, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_formation_strategy" return result, port @@ -1096,7 +1097,7 @@ async def test_formation_strategy_form_new_network( # A new network will be formed mock_app.form_network.assert_called_once() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_formation_strategy_form_initial_network( @@ -1115,7 +1116,7 @@ async def test_formation_strategy_form_initial_network( # A new network will be formed mock_app.form_network.assert_called_once() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) @@ -1143,7 +1144,7 @@ async def test_onboarding_auto_formation_new_hardware( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "zigbee radio" assert result["data"] == { "device": { @@ -1170,7 +1171,7 @@ async def test_formation_strategy_reuse_settings( # Nothing will be written when settings are reused mock_app.write_network_info.assert_not_called() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @patch("homeassistant.components.zha.config_flow.process_uploaded_file") @@ -1200,7 +1201,7 @@ async def test_formation_strategy_restore_manual_backup_non_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1215,7 +1216,7 @@ async def test_formation_strategy_restore_manual_backup_non_ezsp( mock_app.backups.restore_backup.assert_called_once() allow_overwrite_ieee_mock.assert_not_called() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_RADIO_TYPE] == "znp" @@ -1232,7 +1233,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1244,7 +1245,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "maybe_confirm_ezsp_restore" result4 = await hass.config_entries.flow.async_configure( @@ -1255,7 +1256,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( allow_overwrite_ieee_mock.assert_called_once() mock_app.backups.restore_backup.assert_called_once() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1272,7 +1273,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" backup = zigpy.backups.NetworkBackup() @@ -1286,7 +1287,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "maybe_confirm_ezsp_restore" result4 = await hass.config_entries.flow.async_configure( @@ -1297,7 +1298,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( allow_overwrite_ieee_mock.assert_not_called() mock_app.backups.restore_backup.assert_called_once_with(backup) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1313,7 +1314,7 @@ async def test_formation_strategy_restore_manual_backup_invalid_upload( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1327,7 +1328,7 @@ async def test_formation_strategy_restore_manual_backup_invalid_upload( mock_app.backups.restore_backup.assert_not_called() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "upload_manual_backup" assert result3["errors"]["base"] == "invalid_backup_json" @@ -1372,7 +1373,7 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" result3 = await hass.config_entries.flow.async_configure( @@ -1382,7 +1383,7 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "maybe_confirm_ezsp_restore" result4 = await hass.config_entries.flow.async_configure( @@ -1392,7 +1393,7 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( mock_app.backups.restore_backup.assert_called_once() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1428,7 +1429,7 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" # We don't prompt for overwriting the IEEE address, since only EZSP needs this @@ -1450,7 +1451,7 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( mock_app.backups.restore_backup.assert_called_once_with(backup) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_RADIO_TYPE] == "znp" @@ -1480,7 +1481,7 @@ async def test_ezsp_restore_without_settings_change_ieee( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1496,7 +1497,7 @@ async def test_ezsp_restore_without_settings_change_ieee( allow_overwrite_ieee_mock.assert_not_called() mock_app.backups.restore_backup.assert_called_once_with(backup, create_new=False) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1552,7 +1553,7 @@ async def test_options_flow_defaults( mock_async_unload.assert_called_once_with(entry.entry_id) # Unload it ourselves - entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) # Reconfigure ZHA assert result1["step_id"] == "prompt_migrate_or_reconfigure" @@ -1609,7 +1610,7 @@ async def test_options_flow_defaults( ) await hass.async_block_till_done() - assert result6["type"] == FlowResultType.CREATE_ENTRY + assert result6["type"] is FlowResultType.CREATE_ENTRY assert result6["data"] == {} # The updated entry contains correct settings @@ -1735,7 +1736,7 @@ async def test_options_flow_restarts_running_zha_if_cancelled( flow["flow_id"], user_input={} ) - entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( @@ -1790,7 +1791,7 @@ async def test_options_flow_migration_reset_old_adapter( flow["flow_id"], user_input={} ) - entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( @@ -1884,7 +1885,7 @@ async def test_probe_wrong_firmware_installed(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_firmware_installed" @@ -1906,7 +1907,7 @@ async def test_discovery_wrong_firmware_installed(hass: HomeAssistant) -> None: data={}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_firmware_installed" diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index a1b320097e8..5f6dac885f2 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -6,8 +6,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha import zigpy.types -import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import closures, general import zigpy.zcl.foundation as zcl_f from homeassistant.components.cover import ( diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 48eecdd87d4..fefc68a8d94 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha import zigpy.types -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general import zigpy.zdo.types as zdo_t from homeassistant.components.zha.core.const import ( @@ -115,8 +115,7 @@ async def ota_zha_device(zha_device_restored, zigpy_device_mock): "test model", ) - zha_device = await zha_device_restored(zigpy_dev) - return zha_device + return await zha_device_restored(zigpy_dev) def _send_time_changed(hass, seconds): diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index a7b66dea8d7..bc478532859 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -6,11 +6,10 @@ import pytest from pytest_unordered import unordered from zhaquirks.inovelli.VZM31SN import InovelliVZM31SNv11 import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import general, security import zigpy.zcl.foundation as zcl_f -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.zha import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 89ea788e5ef..64360c8b2ff 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general from homeassistant.components.device_tracker import SourceType from homeassistant.components.zha.core.registries import ( diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index f9141795ef1..2cb7c8c94e7 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -7,9 +7,9 @@ from unittest.mock import patch import pytest from zigpy.application import ControllerApplication import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 3493d772a6f..50b07b70e8d 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -3,8 +3,8 @@ from unittest.mock import patch import pytest -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.security as security +from zigpy.profiles import zha +from zigpy.zcl.clusters import security from homeassistant.components.diagnostics import REDACTED from homeassistant.components.zha.core.device import ZHADevice diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index f9242eb1d96..a7e466f1caa 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -40,7 +40,7 @@ import zigpy.zcl.clusters.general import zigpy.zcl.clusters.security import zigpy.zcl.foundation as zcl_f -import homeassistant.components.zha.core.cluster_handlers as cluster_handlers +from homeassistant.components.zha.core import cluster_handlers import homeassistant.components.zha.core.const as zha_const from homeassistant.components.zha.core.device import ZHADevice import homeassistant.components.zha.core.discovery as disc @@ -420,7 +420,7 @@ def _test_single_input_cluster_device_class(probe_mock): (Platform.BINARY_SENSOR, ias_ch), (Platform.SENSOR, analog_ch), ) - for call, details in zip(probe_mock.call_args_list, probes): + for call, details in zip(probe_mock.call_args_list, probes, strict=False): platform, ch = details assert call[0][0] == platform assert call[0][1] == ch @@ -1001,7 +1001,7 @@ async def test_quirks_v2_metadata_errors( # 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: + except ValueError: # 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( @@ -1010,7 +1010,7 @@ async def test_quirks_v2_metadata_errors( "TRADFRI remote control4", ) ) - raise e + raise class BadDeviceClass(enum.Enum): diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 182cc2c4752..666594bd854 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -5,10 +5,9 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from zigpy.application import ControllerApplication -import zigpy.profiles.zha as zha +from zigpy.profiles import zha import zigpy.types -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.zcl.clusters import general, lighting import zigpy.zdo.types from homeassistant.components.zha.core.gateway import ZHAGateway @@ -61,8 +60,7 @@ def required_platform_only(): async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): """ZHA device with just a basic cluster.""" - zha_device = await zha_device_restored(zigpy_dev_basic) - return zha_device + return await zha_device_restored(zigpy_dev_basic) @pytest.fixture diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index fed8fe5bb91..0615fefd644 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -6,11 +6,10 @@ from unittest.mock import patch import pytest import voluptuous_serialize -import zigpy.profiles.zha as zha +from zigpy.profiles import 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 +from zigpy.zcl.clusters import general, lighting from homeassistant.components.zha.core.helpers import ( cluster_command_schema_to_vol_schema, diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 99d6a78924b..70ba88ee6e7 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -255,10 +255,11 @@ async def test_zha_retry_unique_ids( lambda hass, delay, action: async_call_later(hass, 0, action), ): 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) # Wait for the config entry setup to retry await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_connect.mock_calls) == 2 diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index a6473c6007c..762ab14cbaa 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -4,9 +4,8 @@ from datetime import timedelta from unittest.mock import AsyncMock, call, patch, sentinel import pytest -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.profiles import zha +from zigpy.zcl.clusters import general, lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import ( @@ -1631,10 +1630,7 @@ async def test_zha_group_light_entity( device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) device_3_entity_id = find_entity_id(Platform.LIGHT, device_light_3, hass) - assert ( - device_1_entity_id != device_2_entity_id - and device_1_entity_id != device_3_entity_id - ) + assert device_1_entity_id not in (device_2_entity_id, device_3_entity_id) assert device_2_entity_id != device_3_entity_id group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 52b1d891dfd..b16d7a31828 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -4,8 +4,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import closures, general import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 889c73362ae..317e10346f0 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general from homeassistant.components.zha.core.const import ZHA_EVENT from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, Platform diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index a9fb3dd9509..b3fc42c35df 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -5,8 +5,7 @@ from unittest.mock import call, patch import pytest from zigpy.exceptions import ZigbeeException from zigpy.profiles import zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.zcl.clusters import general, lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN @@ -62,7 +61,7 @@ def zigpy_analog_output_device(zigpy_device_mock): async def light(zigpy_device_mock): """Siren fixture.""" - zigpy_device = zigpy_device_mock( + return zigpy_device_mock( { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, @@ -80,8 +79,6 @@ async def light(zigpy_device_mock): node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", ) - return zigpy_device - async def test_number( hass: HomeAssistant, zha_device_joined_restored, zigpy_analog_output_device diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index dbea454ecb0..0363821ac47 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -10,11 +10,11 @@ import zigpy.config from zigpy.config import CONF_DEVICE_PATH import zigpy.types -from homeassistant import config_entries from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.core.const import DOMAIN, RadioType from homeassistant.components.zha.radio_manager import ProbeResult, ZhaRadioManager +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -224,7 +224,7 @@ async def test_migrate_matching_port_config_entry_not_loaded( title="Test", ) config_entry.add_to_hass(hass) - config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) + config_entry.mock_state(hass, ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { @@ -284,7 +284,7 @@ async def test_migrate_matching_port_retry( title="Test", ) config_entry.add_to_hass(hass) - config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) + config_entry.mock_state(hass, ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { @@ -389,7 +389,7 @@ async def test_migrate_initiate_failure( title="Test", ) config_entry.add_to_hass(hass) - config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) + config_entry.mock_state(hass, ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 29020aa4313..279975a260f 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -9,8 +9,8 @@ import pytest import zigpy.quirks as zigpy_quirks from homeassistant.components.zha.binary_sensor import IASZone +from homeassistant.components.zha.core import registries from homeassistant.components.zha.core.const import ATTR_QUIRK_ID -import homeassistant.components.zha.core.registries as registries from homeassistant.helpers import entity_registry as er if typing.TYPE_CHECKING: diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index fea68be86cb..5b57ec7fcc2 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -12,7 +12,7 @@ from zigpy.application import ControllerApplication import zigpy.backups from zigpy.exceptions import NetworkSettingsInconsistent -from homeassistant.components.homeassistant_sky_connect import ( +from homeassistant.components.homeassistant_sky_connect.const import ( DOMAIN as SKYCONNECT_DOMAIN, ) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN @@ -59,8 +59,10 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: "pid": "EA60", "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", "manufacturer": "Nabu Casa", - "description": "SkyConnect v1.0", + "product": "SkyConnect v1.0", + "firmware": "ezsp", }, + version=2, domain=SKYCONNECT_DOMAIN, options={}, title="Home Assistant SkyConnect", @@ -74,8 +76,10 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: "pid": "EA60", "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", "manufacturer": "Nabu Casa", - "description": "Home Assistant Connect ZBT-1", + "product": "Home Assistant Connect ZBT-1", + "firmware": "ezsp", }, + version=2, domain=SKYCONNECT_DOMAIN, options={}, title="Home Assistant Connect ZBT-1", @@ -154,7 +158,7 @@ async def test_multipan_firmware_repair( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(config_entry.entry_id) @@ -203,7 +207,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -241,7 +245,7 @@ async def test_multipan_firmware_retry_on_probe_ezsp( await hass.async_block_till_done() # The config entry state is `SETUP_RETRY`, not `SETUP_ERROR`! - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -265,17 +269,27 @@ async def test_no_warn_on_socket(hass: HomeAssistant) -> None: mock_probe.assert_not_called() -async def test_probe_failure_exception_handling(caplog) -> None: +async def test_probe_failure_exception_handling( + caplog: pytest.LogCaptureFixture, +) -> None: """Test that probe failures are handled gracefully.""" + logger = logging.getLogger( + "homeassistant.components.zha.repairs.wrong_silabs_firmware" + ) + orig_level = logger.level + with ( + caplog.at_level(logging.DEBUG), patch( "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", side_effect=RuntimeError(), - ), - caplog.at_level(logging.DEBUG), + ) as mock_probe_app_type, ): + logger.setLevel(logging.DEBUG) await probe_silabs_firmware_type("/dev/ttyZigbee") + logger.setLevel(orig_level) + mock_probe_app_type.assert_awaited() assert "Failed to probe application type" in caplog.text @@ -308,7 +322,7 @@ async def test_inconsistent_settings_keep_new( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(config_entry.entry_id) @@ -388,7 +402,7 @@ async def test_inconsistent_settings_restore_old( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index bb1c5ca270a..1d3811d0293 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -11,13 +11,12 @@ from zhaquirks import ( PROFILE_ID, ) from zigpy.const import SIG_EP_PROFILE -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 import zigpy.types as t -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -import zigpy.zcl.clusters.security as security from homeassistant.components.zha.select import AqaraMotionSensitivities from homeassistant.const import STATE_UNKNOWN, EntityCategory, Platform @@ -73,7 +72,7 @@ async def siren(hass, zigpy_device_mock, zha_device_joined_restored): async def light(hass, zigpy_device_mock): """Siren fixture.""" - zigpy_device = zigpy_device_mock( + return zigpy_device_mock( { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, @@ -89,8 +88,6 @@ async def light(hass, zigpy_device_mock): node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", ) - return zigpy_device - @pytest.fixture def core_rs(hass_storage): diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 8d0ef8107e3..59da8332b27 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -672,7 +672,6 @@ def core_rs(hass_storage): } ], } - return return _storage diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index f5486d91c0f..652955ef98d 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -5,10 +5,9 @@ from unittest.mock import ANY, call, patch import pytest from zigpy.const import SIG_EP_PROFILE -import zigpy.profiles.zha as zha +from zigpy.profiles import zha import zigpy.zcl -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import general, security import zigpy.zcl.foundation as zcl_f from homeassistant.components.siren import ( diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 644062198f9..c8c2842c400 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -11,12 +11,11 @@ from zhaquirks.const import ( PROFILE_ID, ) from zigpy.exceptions import ZigbeeException -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks import _DEVICE_REGISTRY, CustomCluster, CustomDevice from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 import zigpy.types as t -import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import closures, general from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster import zigpy.zcl.foundation as zcl_f diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 854c08985ac..32be013e673 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -7,10 +7,10 @@ from zigpy.exceptions import DeliveryError from zigpy.ota import OtaImageWithMetadata import zigpy.ota.image as firmware from zigpy.ota.providers import BaseOtaImageMetadata -import zigpy.profiles.zha as zha +from zigpy.profiles import zha import zigpy.types as t -import zigpy.zcl.clusters.general as general -import zigpy.zcl.foundation as foundation +from zigpy.zcl import foundation +from zigpy.zcl.clusters import general from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -229,7 +229,7 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): kwargs=kwargs, ) - ota_packet = t.ZigbeePacket( + return t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=zigpy_device.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), @@ -242,8 +242,6 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): rssi=-30, ) - return ota_packet - @patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01) async def test_firmware_update_success( diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 9cd475a7bf8..927da4ed2c0 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -15,9 +15,8 @@ import zigpy.profiles.zha import zigpy.types from zigpy.types.named import EUI64 import zigpy.util -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.general import Groups -import zigpy.zcl.clusters.security as security import zigpy.zdo.types as zdo_types from homeassistant.components.websocket_api import const @@ -303,7 +302,7 @@ async def test_update_zha_config( app_controller: ControllerApplication, ) -> None: """Test updating ZHA custom configuration.""" - configuration: dict = deepcopy(CONFIG_WITH_ALARM_OPTIONS) + configuration: dict = deepcopy(BASE_CUSTOM_CONFIGURATION) configuration["data"]["zha_options"]["default_light_transition"] = 10 with patch( @@ -318,8 +317,8 @@ async def test_update_zha_config( await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() - configuration = msg["result"] - assert configuration == configuration + test_configuration = msg["result"] + assert test_configuration == configuration await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/zodiac/test_config_flow.py b/tests/components/zodiac/test_config_flow.py index 15e8bb04ef6..e027229def9 100644 --- a/tests/components/zodiac/test_config_flow.py +++ b/tests/components/zodiac/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Zodiac" assert result.get("data") == {} assert result.get("options") == {} @@ -51,5 +51,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index 8987481f460..7e42f41f119 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -64,16 +64,13 @@ async def test_if_fires_on_zone_enter(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" + " - {{ trigger.id }}" ) }, }, @@ -143,16 +140,13 @@ async def test_if_fires_on_zone_enter_uuid(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" + " - {{ trigger.id }}" ) }, }, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 98453071bc1..dbf7357d4a0 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -966,8 +966,7 @@ def aeotec_radiator_thermostat_fixture(client, aeotec_radiator_thermostat_state) def nortek_thermostat_added_event_fixture(client): """Mock a Nortek thermostat node added event.""" event_data = json.loads(load_fixture("zwave_js/nortek_thermostat_added_event.json")) - event = Event("node added", event_data) - return event + return Event("node added", event_data) @pytest.fixture(name="nortek_thermostat_removed_event") @@ -976,8 +975,7 @@ def nortek_thermostat_removed_event_fixture(client): event_data = json.loads( load_fixture("zwave_js/nortek_thermostat_removed_event.json") ) - event = Event("node removed", event_data) - return event + return Event("node removed", event_data) @pytest.fixture(name="integration") diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 511fb8d7570..8da17e228be 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -19,6 +19,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import ADDON_SLUG, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -193,7 +194,7 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with ( patch( @@ -212,7 +213,7 @@ async def test_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Z-Wave JS" assert result2["data"] == { "url": "ws://localhost:3000", @@ -277,7 +278,7 @@ async def test_manual_errors( entry = integration result = await getattr(hass.config_entries, flow).async_init(**flow_params(entry)) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await getattr(hass.config_entries, flow).async_configure( @@ -287,7 +288,7 @@ async def test_manual_errors( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {"base": error} @@ -310,7 +311,7 @@ async def test_manual_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( @@ -320,7 +321,7 @@ async def test_manual_already_configured(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://1.1.1.1:3001" assert entry.data["use_addon"] is False @@ -366,7 +367,7 @@ async def test_supervisor_discovery( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -402,7 +403,7 @@ async def test_supervisor_discovery_cannot_connect( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -433,20 +434,20 @@ async def test_clean_discovery_on_user_create( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" with ( @@ -467,7 +468,7 @@ async def test_clean_discovery_on_user_create( await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 0 - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://localhost:3000", @@ -507,7 +508,7 @@ async def test_abort_discovery_with_existing_entry( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Assert that the entry data is updated with discovery info. assert entry.data["url"] == "ws://host1:3001" @@ -522,7 +523,7 @@ async def test_abort_hassio_discovery_with_existing_flow( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -536,7 +537,7 @@ async def test_abort_hassio_discovery_with_existing_flow( ), ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -559,7 +560,7 @@ async def test_abort_hassio_discovery_for_other_addon( ), ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "not_zwave_js_addon" @@ -580,12 +581,12 @@ async def test_usb_discovery( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -595,7 +596,7 @@ async def test_usb_discovery( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -622,7 +623,7 @@ async def test_usb_discovery( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -640,7 +641,7 @@ async def test_usb_discovery( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -674,12 +675,12 @@ async def test_usb_discovery_addon_not_running( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" # Make sure the discovered usb device is preferred. @@ -715,7 +716,7 @@ async def test_usb_discovery_addon_not_running( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -733,7 +734,7 @@ async def test_usb_discovery_addon_not_running( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -772,11 +773,11 @@ async def test_discovery_addon_not_running( ) assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -804,7 +805,7 @@ async def test_discovery_addon_not_running( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -822,7 +823,7 @@ async def test_discovery_addon_not_running( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -860,12 +861,12 @@ async def test_discovery_addon_not_installed( ) assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["step_id"] == "install_addon" - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS await hass.async_block_till_done() @@ -873,7 +874,7 @@ async def test_discovery_addon_not_installed( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -901,7 +902,7 @@ async def test_discovery_addon_not_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -919,7 +920,7 @@ async def test_discovery_addon_not_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -950,7 +951,7 @@ async def test_abort_usb_discovery_with_existing_flow( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" result2 = await hass.config_entries.flow.async_init( @@ -958,7 +959,7 @@ async def test_abort_usb_discovery_with_existing_flow( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -979,7 +980,7 @@ async def test_abort_usb_discovery_already_configured( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -990,7 +991,7 @@ async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_requires_supervisor" @@ -1003,7 +1004,7 @@ async def test_usb_discovery_already_running( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1020,7 +1021,7 @@ async def test_abort_usb_discovery_aborts_specific_devices( context={"source": config_entries.SOURCE_USB}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zwave_device" @@ -1031,14 +1032,14 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" with ( @@ -1058,7 +1059,7 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://localhost:3000", @@ -1093,7 +1094,7 @@ async def test_addon_running( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" with ( @@ -1110,7 +1111,7 @@ async def test_addon_running( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -1181,14 +1182,14 @@ async def test_addon_running_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason @@ -1227,14 +1228,14 @@ async def test_addon_running_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == "/test_new" @@ -1260,14 +1261,14 @@ async def test_addon_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1295,7 +1296,7 @@ async def test_addon_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -1313,7 +1314,7 @@ async def test_addon_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -1348,14 +1349,14 @@ async def test_addon_installed_start_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1383,7 +1384,7 @@ async def test_addon_installed_start_failure( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1391,7 +1392,7 @@ async def test_addon_installed_start_failure( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -1423,14 +1424,14 @@ async def test_addon_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1458,7 +1459,7 @@ async def test_addon_installed_failures( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1466,7 +1467,7 @@ async def test_addon_installed_failures( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -1489,14 +1490,14 @@ async def test_addon_installed_set_options_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1524,7 +1525,7 @@ async def test_addon_installed_set_options_failure( }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_set_config_failed" assert start_addon.call_count == 0 @@ -1561,14 +1562,14 @@ async def test_addon_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1596,7 +1597,7 @@ async def test_addon_installed_already_configured( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1604,7 +1605,7 @@ async def test_addon_installed_already_configured( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == "/new" @@ -1630,14 +1631,14 @@ async def test_addon_not_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -1647,7 +1648,7 @@ async def test_addon_not_installed( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1675,7 +1676,7 @@ async def test_addon_not_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -1693,7 +1694,7 @@ async def test_addon_not_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -1719,14 +1720,14 @@ async def test_install_addon_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS # Make sure the flow continues when the progress task is done. await hass.async_block_till_done() @@ -1735,7 +1736,7 @@ async def test_install_addon_failure( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -1749,7 +1750,7 @@ async def test_options_manual(hass: HomeAssistant, client, integration) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.options.async_configure( @@ -1757,7 +1758,7 @@ async def test_options_manual(hass: HomeAssistant, client, integration) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://1.1.1.1:3001" assert entry.data["use_addon"] is False assert entry.data["integration_created_addon"] is False @@ -1774,7 +1775,7 @@ async def test_options_manual_different_device( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.options.async_configure( @@ -1782,7 +1783,7 @@ async def test_options_manual_different_device( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "different_device" @@ -1798,14 +1799,14 @@ async def test_options_not_addon( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.options.async_configure( @@ -1816,7 +1817,7 @@ async def test_options_not_addon( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://localhost:3000" assert entry.data["use_addon"] is False assert entry.data["integration_created_addon"] is False @@ -1908,14 +1909,14 @@ async def test_options_addon_running( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -1931,7 +1932,7 @@ async def test_options_addon_running( ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1940,7 +1941,7 @@ async def test_options_addon_running( assert restart_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == new_addon_options["device"] assert entry.data["s0_legacy_key"] == new_addon_options["s0_legacy_key"] @@ -2017,14 +2018,14 @@ async def test_options_addon_running_no_changes( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2037,7 +2038,7 @@ async def test_options_addon_running_no_changes( assert set_addon_options.call_count == 0 assert restart_addon.call_count == 0 - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == new_addon_options["device"] assert entry.data["s0_legacy_key"] == new_addon_options["s0_legacy_key"] @@ -2160,14 +2161,14 @@ async def test_options_different_device( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2183,7 +2184,7 @@ async def test_options_different_device( {"options": new_addon_options}, ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2205,7 +2206,7 @@ async def test_options_different_device( "core_zwave_js", {"options": addon_options}, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2216,7 +2217,7 @@ async def test_options_different_device( result = await hass.config_entries.options.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "different_device" assert entry.data == data assert client.connect.call_count == 2 @@ -2318,14 +2319,14 @@ async def test_options_addon_restart_failed( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2341,7 +2342,7 @@ async def test_options_addon_restart_failed( {"options": new_addon_options}, ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2360,7 +2361,7 @@ async def test_options_addon_restart_failed( "core_zwave_js", {"options": old_addon_options}, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2371,7 +2372,7 @@ async def test_options_addon_restart_failed( result = await hass.config_entries.options.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" assert entry.data == data assert client.connect.call_count == 2 @@ -2445,14 +2446,14 @@ async def test_options_addon_running_server_info_failure( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2461,7 +2462,7 @@ async def test_options_addon_running_server_info_failure( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert entry.data == data assert client.connect.call_count == 2 @@ -2553,14 +2554,14 @@ async def test_options_addon_not_installed( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -2570,7 +2571,7 @@ async def test_options_addon_not_installed( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2586,7 +2587,7 @@ async def test_options_addon_not_installed( ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2598,7 +2599,7 @@ async def test_options_addon_not_installed( await hass.async_block_till_done() await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == new_addon_options["device"] assert entry.data["s0_legacy_key"] == new_addon_options["s0_legacy_key"] @@ -2627,14 +2628,14 @@ async def test_import_addon_installed( data={"usb_path": "/test/imported", "network_key": "imported123"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" # the default input should be the imported data @@ -2666,7 +2667,7 @@ async def test_import_addon_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -2684,7 +2685,7 @@ async def test_import_addon_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -2717,7 +2718,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" with ( @@ -2732,7 +2733,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://127.0.0.1:3000", diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 054906cd0f6..ea354ab80d3 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -128,7 +128,9 @@ async def test_device_diagnostics( ) assert diagnostics_data["state"] == { **multisensor_6.data, - "values": {id: val.data for id, val in multisensor_6.values.items()}, + "values": { + value_id: val.data for value_id, val in multisensor_6.values.items() + }, "endpoints": { str(idx): endpoint.data for idx, endpoint in multisensor_6.endpoints.items() }, diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 822302a9940..85611262214 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -673,7 +673,7 @@ async def test_addon_options_changed( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.data["usb_path"] == new_device assert entry.data["s0_legacy_key"] == new_s0_legacy_key assert entry.data["s2_access_control_key"] == new_s2_access_control_key diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index c5cfba18569..338d1511fc3 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -650,20 +650,25 @@ async def test_update_entity_delay( assert len(client.async_send_command.call_args_list) == 2 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) + + nodes: set[int] = set() assert len(client.async_send_command.call_args_list) == 3 args = client.async_send_command.call_args_list[2][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == ge_in_wall_dimmer_switch.node_id + nodes.add(args["nodeId"]) 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 len(client.async_send_command.call_args_list) == 4 args = client.async_send_command.call_args_list[3][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + nodes.add(args["nodeId"]) + + assert len(nodes) == 2 + assert nodes == {ge_in_wall_dimmer_switch.node_id, zen_31.node_id} async def test_update_entity_partial_restore_data( diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 52d5e8fce6f..a71df8751b6 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -79,7 +79,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -90,7 +90,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -107,7 +107,7 @@ async def test_error_handling_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_valid_uuid_set" @@ -117,7 +117,7 @@ async def test_handle_error_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -148,7 +148,7 @@ async def test_duplicate_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -157,7 +157,7 @@ async def test_duplicate_user(hass: HomeAssistant) -> None: "token": "test-token", }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -183,5 +183,5 @@ async def test_duplicate_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/conftest.py b/tests/conftest.py index 157e0f2ba59..3a95e0e58b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,6 +62,7 @@ from homeassistant.helpers import ( label_registry as lr, recorder as recorder_helper, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import BASE_PLATFORMS, async_setup_component from homeassistant.util import location @@ -903,26 +904,45 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient, None, self.rc = 0 with patch("paho.mqtt.client.Client") as mock_client: + # The below use a call_soon for the on_publish/on_subscribe/on_unsubscribe + # callbacks to simulate the behavior of the real MQTT client which will + # not be synchronous. @ha.callback def _async_fire_mqtt_message(topic, payload, qos, retain): async_fire_mqtt_message(hass, topic, payload, qos, retain) mid = get_mid() - mock_client.on_publish(0, 0, mid) + hass.loop.call_soon(mock_client.on_publish, 0, 0, mid) return FakeInfo(mid) def _subscribe(topic, qos=0): mid = get_mid() - mock_client.on_subscribe(0, 0, mid) + hass.loop.call_soon(mock_client.on_subscribe, 0, 0, mid) return (0, mid) def _unsubscribe(topic): mid = get_mid() - mock_client.on_unsubscribe(0, 0, mid) + hass.loop.call_soon(mock_client.on_unsubscribe, 0, 0, mid) return (0, mid) + def _connect(*args, **kwargs): + # Connect always calls reconnect once, but we + # mock it out so we call reconnect to simulate + # the behavior. + mock_client.reconnect() + hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ) + mock_client.on_socket_open( + mock_client, None, Mock(fileno=Mock(return_value=-1)) + ) + mock_client.on_socket_register_write( + mock_client, None, Mock(fileno=Mock(return_value=-1)) + ) + return 0 + mock_client = mock_client.return_value - mock_client.connect.return_value = 0 + mock_client.connect.side_effect = _connect mock_client.subscribe.side_effect = _subscribe mock_client.unsubscribe.side_effect = _unsubscribe mock_client.publish.side_effect = _async_fire_mqtt_message @@ -984,8 +1004,9 @@ async def _mqtt_mock_entry( # connected set to True to get a more realistic behavior when subscribing mock_mqtt_instance.connected = True + mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, 0, 0) - hass.helpers.dispatcher.async_dispatcher_send(mqtt.MQTT_CONNECTED) + async_dispatcher_send(hass, mqtt.MQTT_CONNECTED) await hass.async_block_till_done() return mock_mqtt_instance diff --git a/tests/fixtures/non_packaged_scripts/alexa_locales.txt b/tests/fixtures/non_packaged_scripts/alexa_locales.txt new file mode 100644 index 00000000000..beb9c8dbc7e --- /dev/null +++ b/tests/fixtures/non_packaged_scripts/alexa_locales.txt @@ -0,0 +1,650 @@ +

List of Alexa Interfaces and Supported Languages

+ + +
+ + + + + +

Implement the Alexa interfaces to build automotive skills, music, radio, and podcast skills, smart home skills, and video skills. Alexa interfaces use the pre-built voice interaction model.

+ +

You can use these interfaces with Alexa Voice Service (AVS) Built-in and Alexa Connect Kit (ACK) enabled devices, also. For more details, see Smart Home Development Options.

+ +

Alexa interfaces

+ +

The following table shows the interfaces that you can implement in your Alexa skills. Follow the link to each interface for full details, including the supported capabilities and example customer utterances.

+ + + + +
Interface + Version + Primary skill type + Supported languages + +
+

Alexa.ApplicationStateReporter

+
+

1.0

+
+

AVS

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Audio.PlayQueue

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.AuthorizationController

+
+

1.0

+
+

Automotive

+
+

en-CA, en-US, es-MX, es-US, fr-CA

+ +
+

Alexa.AutomationManagement

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.Automotive.VehicleData

+
+

1.0

+
+

Automotive

+
+

en-CA, en-US, es-MX, es-US, fr-CA

+ +
+

Alexa.BrightnessController

+
+

3

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX,es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Camera.LiveViewController

+
+

1.7

+
+

AVS

+
+

en-US

+ +
+

Alexa.CameraStreamController

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ChannelController

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ColorController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ColorTemperatureController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Commissionable

+
+

1.0

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ConsentManagement.ConsentRequiredReporter

+
+

1.0

+
+

Smart Home

+
+

ja-JP

+ +
+

Alexa.ContactSensor

+
+

3

+
+

Smart Home Security

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Cooking

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.FoodTemperatureController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.FoodTemperatureSensor

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.PresetController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.TemperatureController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.TemperatureSensor

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.TimeController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.DataController

+
+

1.0

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.DeviceUsage.Estimation

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.DeviceUsage.Meter

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.DoorbellEventSource

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.EndpointHealth

+
+

3.1

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.EqualizerController

+
+

3

+
+

Smart Home Entertainment

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.InputController

+
+

3

+
+

Smart Home Entertainment,
+Video, AVS

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.InventoryLevelSensor

+
+

3

+
+

Smart Home

+
+

de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.InventoryLevelUsageSensor

+
+

3

+
+

Smart Home

+
+

de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.InventoryUsageSensor

+
+

3

+
+

Smart Home

+
+

de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.KeypadController

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Launcher

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.LockController

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Media.Playback

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.Media.PlayQueue

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.Media.Search

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.ModeController

+
+

3

+
+

Smart Home, AVS

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.MotionSensor

+
+

3

+
+

Smart Home Security

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PercentageController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PlaybackController

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PlaybackStateReporter

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PowerController

+
+

3

+
+

Smart Home,
+Video, AVS

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PowerLevelController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.ProactiveNotificationSource

+
+

3.0

+
+

Smart Home

+
+

Notifications for device state: de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT
+Notifications for cooking: en-US

+ +
+

Alexa.RangeController

+
+

3

+
+

Smart Home, AVS

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.RecordController

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.RemoteVideoPlayer

+
+

3.1

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.RTCSessionController

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-US, es-ES, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SceneController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SecurityPanelController

+
+

3

+
+

Smart Home Security

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SecurityPanelController.Alert

+
+

1.1

+
+

Smart Home Security

+
+

de-DE, en-CA, en-GB, en-US, es-US, fr-CA, fr-FR

+ +
+

Alexa.SeekController

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SimpleEventSource

+
+

1.0

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.SmartVision.ObjectDetectionSensor

+
+

1.0

+
+

Smart Home Security

+
+

en-US

+ +
+

Alexa.Speaker

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, it-IT, ja-JP

+ +
+

Alexa.StepSpeaker

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, it-IT

+ +
+

Alexa.TemperatureSensor

+
+

3

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ThermostatController

+
+

3.1

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ThermostatController.Configuration

+
+

3

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.ThermostatController.HVAC.Components

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.ThermostatController.Schedule

+
+

3.2

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.TimeHoldController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.ToggleController

+
+

3

+
+

Smart Home, AVS

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.UIController

+
+

3.0

+
+

Video

+
+

en-US

+ +
+

Alexa.UserPreference

+
+

2.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.VideoRecorder

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.WakeOnLANController

+
+

3

+
+

Smart Home Entertainment

+
+

de-DE, en-AU, en-IN, en-US, es-ES, it-IT, ja-JP

+ +
+ + + + diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index 7cb08621e83..f3b008a6113 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -11,7 +11,7 @@ from script.hassfest.requirements import validate_requirements_format @pytest.fixture def integration(): """Fixture for hassfest integration model.""" - integration = Integration( + return Integration( path=Path("homeassistant/components/test"), _manifest={ "domain": "test", @@ -21,7 +21,6 @@ def integration(): "requirements": [], }, ) - return integration def test_validate_requirements_format_with_space(integration: Integration) -> None: diff --git a/tests/hassfest/test_translations.py b/tests/hassfest/test_translations.py new file mode 100644 index 00000000000..526320a5044 --- /dev/null +++ b/tests/hassfest/test_translations.py @@ -0,0 +1,21 @@ +"""Tests for hassfest translations.""" + +import pytest +import voluptuous as vol + +from script.hassfest import translations + + +def test_string_with_no_placeholders_in_single_quotes() -> None: + """Test string with no placeholders in single quotes.""" + schema = vol.Schema(translations.string_no_single_quoted_placeholders) + + with pytest.raises(vol.Invalid): + schema("This has '{placeholder}' in single quotes") + + for value in ( + 'This has "{placeholder}" in double quotes', + "Simple {placeholder}", + "No placeholder", + ): + schema(value) diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 1ee8a42b6b9..22f1dc8e534 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,5 +1,6 @@ """Tests for the Area Registry.""" +from functools import partial from typing import Any import pytest @@ -491,3 +492,40 @@ async def test_entries_for_label( assert not ar.async_entries_for_label(area_registry, "unknown") assert not ar.async_entries_for_label(area_registry, "") + + +async def test_async_get_or_create_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to create in the wrong thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_create, "Mock1") + + +async def test_async_update_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to update in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial(area_registry.async_update, area.id, name="Mock2") + ) + + +async def test_async_delete_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to delete in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_delete from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_delete, area.id) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 1b279fd0f51..20dea85c3e4 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -8,7 +8,7 @@ from freezegun import freeze_time import pytest import voluptuous as vol -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -2178,12 +2178,12 @@ def _find_run_id(traces, trace_type, item_id): async def assert_automation_condition_trace(hass_ws_client, automation_id, expected): """Test the result of automation condition.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id client = await hass_ws_client() diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 9816dc38189..5e9fcd9d661 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -544,7 +544,7 @@ def test_string(hass: HomeAssistant) -> None: # Test subclasses of str are returned class MyString(str): - pass + __slots__ = () my_string = MyString("hello") assert schema(my_string) is my_string diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index b53a6d5ec1d..fed48c5735b 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -10,6 +10,7 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers.deprecation import ( + DeprecatedAlias, DeprecatedConstant, DeprecatedConstantEnum, check_if_deprecated_constant, @@ -283,38 +284,59 @@ class TestDeprecatedConstantEnum(StrEnum): TEST = "value" -def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: - if isinstance(obj, tuple): - if len(obj) == 2: - return obj[0].value - - return obj[0] - +def _get_value( + obj: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple[Any, ...], +) -> Any: if isinstance(obj, DeprecatedConstant): return obj.value if isinstance(obj, DeprecatedConstantEnum): return obj.enum.value + if isinstance(obj, DeprecatedAlias): + return obj.value + + if len(obj) == 2: + return obj[0].value + + return obj[0] + @pytest.mark.parametrize( - ("deprecated_constant", "extra_msg"), + ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", + "constant", + ), + ( + DeprecatedAlias(1, "new_alias", None), + ". Use new_alias instead", + "alias", + ), + ( + DeprecatedAlias(1, "new_alias", "2099.1"), + " which will be removed in HA Core 2099.1. Use new_alias instead", + "alias", ), ], ) @@ -330,10 +352,14 @@ def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: ) def test_check_if_deprecated_constant( caplog: pytest.LogCaptureFixture, - deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | tuple, + deprecated_constant: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple, extra_msg: str, module_name: str, extra_extra_msg: str, + description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { @@ -378,28 +404,42 @@ def test_check_if_deprecated_constant( assert ( module_name, logging.WARNING, - f"TEST_CONSTANT was used from hue, this is a deprecated constant{extra_msg}{extra_extra_msg}", + f"TEST_CONSTANT was used from hue, this is a deprecated {description}{extra_msg}{extra_extra_msg}", ) in caplog.record_tuples @pytest.mark.parametrize( - ("deprecated_constant", "extra_msg"), + ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", + "constant", + ), + ( + DeprecatedAlias(1, "new_alias", None), + ". Use new_alias instead", + "alias", + ), + ( + DeprecatedAlias(1, "new_alias", "2099.1"), + " which will be removed in HA Core 2099.1. Use new_alias instead", + "alias", ), ], ) @@ -412,9 +452,13 @@ def test_check_if_deprecated_constant( ) def test_check_if_deprecated_constant_integration_not_found( caplog: pytest.LogCaptureFixture, - deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | tuple, + deprecated_constant: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple, extra_msg: str, module_name: str, + description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { @@ -432,7 +476,7 @@ def test_check_if_deprecated_constant_integration_not_found( assert ( module_name, logging.WARNING, - f"TEST_CONSTANT is a deprecated constant{extra_msg}", + f"TEST_CONSTANT is a deprecated {description}{extra_msg}", ) not in caplog.record_tuples diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index bed3dea4dc1..6b167f8ee49 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2,6 +2,7 @@ from collections.abc import Iterable from contextlib import AbstractContextManager, nullcontext +from functools import partial import time from typing import Any from unittest.mock import patch @@ -11,7 +12,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, HomeAssistant, ReleaseChannel from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( area_registry as ar, @@ -2390,7 +2391,7 @@ async def test_device_name_translation_placeholders( }, }, {"placeholder": "special"}, - "stable", + ReleaseChannel.STABLE, nullcontext(), ( "has translation placeholders '{'placeholder': 'special'}' which do " @@ -2405,7 +2406,7 @@ async def test_device_name_translation_placeholders( }, }, {"placeholder": "special"}, - "beta", + ReleaseChannel.BETA, pytest.raises( HomeAssistantError, match="Missing placeholder '2ndplaceholder'" ), @@ -2419,7 +2420,7 @@ async def test_device_name_translation_placeholders( }, }, None, - "stable", + ReleaseChannel.STABLE, nullcontext(), ( "has translation placeholders '{}' which do " @@ -2434,7 +2435,7 @@ async def test_device_name_translation_placeholders_errors( translation_key: str | None, translations: dict[str, str] | None, placeholders: dict[str, str] | None, - release_channel: str, + release_channel: ReleaseChannel, expectation: AbstractContextManager, expected_error: str, caplog: pytest.LogCaptureFixture, @@ -2473,3 +2474,49 @@ async def test_device_name_translation_placeholders_errors( ) assert expected_error in caplog.text + + +async def test_async_get_or_create_thread_safety( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test async_get_or_create raises when called from wrong thread.""" + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_update_device from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + device_registry.async_get_or_create, + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + ) + + +async def test_async_remove_device_thread_safety( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test async_remove_device raises when called from wrong thread.""" + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_remove_device from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + device_registry.async_remove_device, device.id + ) diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 149231a9368..d9a79cc6a7a 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -239,3 +239,24 @@ async def test_dispatcher_add_dispatcher(hass: HomeAssistant) -> None: async_dispatcher_send(hass, "test", 5) assert calls == [3, 4, 4, 5, 5] + + +async def test_thread_safety_checks(hass: HomeAssistant) -> None: + """Test dispatcher thread safety checks.""" + hass.config.debug = True + calls = [] + + @callback + def _dispatcher(data): + calls.append(data) + + async_dispatcher_connect(hass, "test", _dispatcher) + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_dispatcher_send from a thread.", + ): + await hass.async_add_executor_job(async_dispatcher_send, hass, "test", 3) + + async_dispatcher_send(hass, "test", 4) + assert calls == [4] diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dac03f0be67..a80674e0f76 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -5,6 +5,7 @@ from collections.abc import Iterable import dataclasses from datetime import timedelta from enum import IntFlag +from functools import cached_property import logging import threading from typing import Any @@ -15,7 +16,6 @@ import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol -from homeassistant.backports.functools import cached_property from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -28,6 +28,7 @@ from homeassistant.core import ( HassJobType, HomeAssistant, HomeAssistantError, + ReleaseChannel, callback, ) from homeassistant.helpers import device_registry as dr, entity, entity_registry as er @@ -1249,7 +1250,7 @@ async def test_entity_name_translation_placeholders( }, }, {"placeholder": "special"}, - "stable", + ReleaseChannel.STABLE, ( "has translation placeholders '{'placeholder': 'special'}' which do " "not match the name '{placeholder} English ent {2ndplaceholder}'" @@ -1263,7 +1264,7 @@ async def test_entity_name_translation_placeholders( }, }, {"placeholder": "special"}, - "beta", + ReleaseChannel.BETA, "HomeAssistantError: Missing placeholder '2ndplaceholder'", ), ( @@ -1274,7 +1275,7 @@ async def test_entity_name_translation_placeholders( }, }, None, - "stable", + ReleaseChannel.STABLE, ( "has translation placeholders '{}' which do " "not match the name '{placeholder} English ent'" @@ -1287,7 +1288,7 @@ async def test_entity_name_translation_placeholder_errors( translation_key: str | None, translations: dict[str, str] | None, placeholders: dict[str, str] | None, - release_channel: str, + release_channel: ReleaseChannel, expected_error: str, caplog: pytest.LogCaptureFixture, ) -> None: @@ -2329,30 +2330,30 @@ async def test_cached_entity_properties( async def test_cached_entity_property_delete_attr(hass: HomeAssistant) -> None: """Test deleting an _attr corresponding to a cached property.""" - property = "has_entity_name" + property_name = "has_entity_name" ent = entity.Entity() - assert not hasattr(ent, f"_attr_{property}") + assert not hasattr(ent, f"_attr_{property_name}") with pytest.raises(AttributeError): - delattr(ent, f"_attr_{property}") - assert getattr(ent, property) is False + delattr(ent, f"_attr_{property_name}") + assert getattr(ent, property_name) is False with pytest.raises(AttributeError): - delattr(ent, f"_attr_{property}") - assert not hasattr(ent, f"_attr_{property}") - assert getattr(ent, property) is False + delattr(ent, f"_attr_{property_name}") + assert not hasattr(ent, f"_attr_{property_name}") + assert getattr(ent, property_name) is False - setattr(ent, f"_attr_{property}", True) - assert getattr(ent, property) is True + setattr(ent, f"_attr_{property_name}", True) + assert getattr(ent, property_name) is True - delattr(ent, f"_attr_{property}") - assert not hasattr(ent, f"_attr_{property}") - assert getattr(ent, property) is False + delattr(ent, f"_attr_{property_name}") + assert not hasattr(ent, f"_attr_{property_name}") + assert getattr(ent, property_name) is False async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> None: """Test entity properties on class level work in derived classes.""" - property = "attribution" + property_name = "attribution" values = ["abcd", "efgh"] class EntityWithClassAttribute1(entity.Entity): @@ -2407,15 +2408,15 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No ] for ent in entities: - assert getattr(ent[0], property) == values[0] - assert getattr(ent[1], property) == values[0] + assert getattr(ent[0], property_name) == values[0] + assert getattr(ent[1], property_name) == values[0] # Test update for ent in entities: - setattr(ent[0], f"_attr_{property}", values[1]) + setattr(ent[0], f"_attr_{property_name}", values[1]) for ent in entities: - assert getattr(ent[0], property) == values[1] - assert getattr(ent[1], property) == values[0] + assert getattr(ent[0], property_name) == values[1] + assert getattr(ent[1], property_name) == values[0] async def test_cached_entity_property_override(hass: HomeAssistant) -> None: @@ -2593,3 +2594,50 @@ async def test_get_hassjob_type(hass: HomeAssistant) -> None: 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 + + +async def test_async_write_ha_state_thread_safety(hass: HomeAssistant) -> None: + """Test async_write_ha_state thread safety.""" + hass.config.debug = True + + ent = entity.Entity() + ent.entity_id = "test.any" + ent.hass = hass + ent.async_write_ha_state() + assert hass.states.get(ent.entity_id) + + ent2 = entity.Entity() + ent2.entity_id = "test.any2" + ent2.hass = hass + with pytest.raises( + RuntimeError, + match="Detected code that calls async_write_ha_state from a thread.", + ): + await hass.async_add_executor_job(ent2.async_write_ha_state) + assert not hass.states.get(ent2.entity_id) + + +async def test_async_write_ha_state_thread_safety_custom_component( + hass: HomeAssistant, +) -> None: + """Test async_write_ha_state thread safe for custom components.""" + + ent = entity.Entity() + ent._is_custom_component = True + ent.entity_id = "test.any" + ent.hass = hass + ent.platform = MockEntityPlatform(hass, domain="test") + ent.async_write_ha_state() + assert hass.states.get(ent.entity_id) + + ent2 = entity.Entity() + ent2._is_custom_component = True + ent2.entity_id = "test.any2" + ent2.hass = hass + ent2.platform = MockEntityPlatform(hass, domain="test") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_write_ha_state from a thread.", + ): + await hass.async_add_executor_job(ent2.async_write_ha_state) + assert not hass.states.get(ent2.entity_id) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 59c4f7357f3..64f6d6bf9f5 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1112,6 +1112,19 @@ async def test_entity_registry_updates_invalid_entity_id(hass: HomeAssistant) -> assert hass.states.get("diff_domain.world") is None +async def test_add_entity_with_invalid_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test trying to add an entity with an invalid entity_id.""" + platform = MockEntityPlatform(hass) + entity = MockEntity(entity_id="i.n.v.a.l.i.d") + await platform.async_add_entities([entity]) + assert ( + "Error adding entity i.n.v.a.l.i.d for domain " + "test_domain with platform test_platform" in caplog.text + ) + + async def test_device_info_called( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 91c749a0d7f..bc3b2d6f705 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,6 +1,7 @@ """Tests for the Entity Registry.""" from datetime import timedelta +from functools import partial from typing import Any from unittest.mock import patch @@ -447,6 +448,116 @@ async def test_filter_on_load( assert entry_system_category.entity_category is None +@pytest.mark.parametrize("load_registries", [False]) +async def test_load_bad_data( + hass: HomeAssistant, + hass_storage: dict[str, Any], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading invalid data.""" + hass_storage[er.STORAGE_KEY] = { + "version": er.STORAGE_VERSION_MAJOR, + "minor_version": er.STORAGE_VERSION_MINOR, + "data": { + "entities": [ + { + "aliases": [], + "area_id": None, + "capabilities": None, + "categories": {}, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.test1", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "00001", + "labels": [], + "name": None, + "options": None, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": 123, # Should trigger warning + "unit_of_measurement": None, + }, + { + "aliases": [], + "area_id": None, + "capabilities": None, + "categories": {}, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.test2", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "00002", + "labels": [], + "name": None, + "options": None, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": ["not", "valid"], # Should not load + "unit_of_measurement": None, + }, + ], + "deleted_entities": [ + { + "config_entry_id": None, + "entity_id": "test.test3", + "id": "00003", + "orphaned_timestamp": None, + "platform": "super_platform", + "unique_id": 234, # Should trigger warning + }, + { + "config_entry_id": None, + "entity_id": "test.test4", + "id": "00004", + "orphaned_timestamp": None, + "platform": "super_platform", + "unique_id": ["also", "not", "valid"], # Should not load + }, + ], + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + assert len(registry.entities) == 1 + assert set(registry.entities.keys()) == {"test.test1"} + + assert len(registry.deleted_entities) == 1 + assert set(registry.deleted_entities.keys()) == {("test", "super_platform", 234)} + + assert ( + "'test' from integration super_platform has a non string unique_id '123', " + "please create a bug report" in caplog.text + ) + assert ( + "Entity registry entry 'test.test2' from integration super_platform could not " + "be loaded: 'unique_id must be a string, got ['not', 'valid']', please create " + "a bug report" in caplog.text + ) + + def test_async_get_entity_id(entity_registry: er.EntityRegistry) -> None: """Test that entity_id is returned.""" entry = entity_registry.async_get_or_create("light", "hue", "1234") @@ -1472,6 +1583,38 @@ async def test_hidden_by_str_not_allowed(entity_registry: er.EntityRegistry) -> ) +async def test_unique_id_non_hashable( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test unique_id which is not hashable.""" + with pytest.raises(TypeError): + entity_registry.async_get_or_create("light", "hue", ["not", "valid"]) + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(TypeError): + entity_registry.async_update_entity(entity_id, new_unique_id=["not", "valid"]) + + +async def test_unique_id_non_string( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test unique_id which is not a string.""" + entity_registry.async_get_or_create("light", "hue", 1234) + assert ( + "'light' from integration hue has a non string unique_id '1234', " + "please create a bug report" in caplog.text + ) + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + entity_registry.async_update_entity(entity_id, new_unique_id=2345) + assert ( + "'light' from integration hue has a non string unique_id '2345', " + "please create a bug report" in caplog.text + ) + + def test_migrate_entity_to_new_platform( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: @@ -1846,3 +1989,46 @@ async def test_entries_for_category(entity_registry: er.EntityRegistry) -> None: 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", "") + + +async def test_get_or_create_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_get_or_create_from a thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_get_or_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + entity_registry.async_get_or_create, "light", "hue", "1234" + ) + + +async def test_async_update_entity_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_get_or_create from a thread.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update_entity from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + entity_registry.async_update_entity, + entry.entity_id, + new_unique_id="5678", + ) + ) + + +async def test_async_remove_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_remove from a thread.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_remove from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(entity_registry.async_remove, entry.entity_id) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cf5051e657a..07228abcc2c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,12 +15,11 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, 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 from homeassistant.helpers.event import ( - EventStateChangedData, TrackStates, TrackTemplate, TrackTemplateResult, @@ -4805,3 +4804,18 @@ async def test_async_track_device_registry_updated_event_with_a_callback_that_th unsub2() assert event_data[0] == {"action": "create", "device_id": device_id} + + +async def test_track_state_change_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test track_state_change is deprecated.""" + async_track_state_change( + hass, "light.Bowl", lambda entity_id, old_state, new_state: None, "on", "off" + ) + + assert ( + "Detected code that calls `async_track_state_change` instead " + "of `async_track_state_change_event` which is deprecated and " + "will be removed in Home Assistant 2025.5. Please report this issue." + ) in caplog.text diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index fe215264f59..904bed965c8 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -205,3 +205,45 @@ async def test_report_missing_integration_frame( frame.report(what, error_if_core=False, log_custom_component_only=True) assert caplog.text == "" + + +@pytest.mark.parametrize("run_count", [1, 2]) +# Run this twice to make sure the flood check does not +# kick in when error_if_integration=True +async def test_report_error_if_integration( + caplog: pytest.LogCaptureFixture, run_count: int +) -> None: + """Test RuntimeError is raised if error_if_integration is set.""" + frames = 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()", + ), + ] + ) + with ( + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + pytest.raises( + RuntimeError, + match=( + "Detected that integration 'hue' did a bad" + " thing at homeassistant/components/hue/light.py" + ), + ), + ): + frame.report("did a bad thing", error_if_integration=True) diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index e986a07d7d5..5ad5071266b 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -106,8 +106,8 @@ async def test_get_icons(hass: HomeAssistant) -> None: # Ensure icons file for platform isn't loaded, as that isn't supported icons = await icon.async_get_icons(hass, "entity") assert icons == {} - icons = await icon.async_get_icons(hass, "entity", ["test.switch"]) - assert icons == {} + with pytest.raises(ValueError, match="test.switch"): + await icon.async_get_icons(hass, "entity", ["test.switch"]) # Load up an custom integration hass.config.components.add("test_package") diff --git a/tests/helpers/test_importlib.py b/tests/helpers/test_importlib.py index 5683dd5cf94..5c9686233f9 100644 --- a/tests/helpers/test_importlib.py +++ b/tests/helpers/test_importlib.py @@ -41,16 +41,40 @@ async def test_async_import_module_failures(hass: HomeAssistant) -> None: with ( patch( "homeassistant.helpers.importlib.importlib.import_module", - side_effect=ImportError, + side_effect=ValueError, ), - pytest.raises(ImportError), + pytest.raises(ValueError), + ): + await importlib.async_import_module(hass, "test.module") + + mock_module = MockModule() + # The failure should be not be cached + with ( + patch( + "homeassistant.helpers.importlib.importlib.import_module", + return_value=mock_module, + ), + ): + assert await importlib.async_import_module(hass, "test.module") is mock_module + + +async def test_async_import_module_failure_caches_module_not_found( + hass: HomeAssistant, +) -> None: + """Test importing a module caches ModuleNotFound.""" + with ( + patch( + "homeassistant.helpers.importlib.importlib.import_module", + side_effect=ModuleNotFoundError, + ), + pytest.raises(ModuleNotFoundError), ): await importlib.async_import_module(hass, "test.module") mock_module = MockModule() # The failure should be cached with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.helpers.importlib.importlib.import_module", return_value=mock_module, diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index caffebf094e..3c9594bca38 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -362,6 +362,18 @@ 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 pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_cloud=True) + + with patch( + "homeassistant.components.cloud.async_remote_ui_url", + return_value="https://example.nabu.casa", + ): + hass.config.components.add("cloud") + assert ( + _get_external_url(hass, require_cloud=True) == "https://example.nabu.casa" + ) + async def test_get_cloud_url(hass: HomeAssistant) -> None: """Test getting an instance URL when the user has set an external URL.""" diff --git a/tests/helpers/test_registry.py b/tests/helpers/test_registry.py index 46b04b05fe3..0218267452a 100644 --- a/tests/helpers/test_registry.py +++ b/tests/helpers/test_registry.py @@ -21,10 +21,10 @@ class SampleRegistry(BaseRegistry): self._store = storage.Store(hass, 1, "test") self.save_calls = 0 - def _data_to_save(self) -> None: + def _data_to_save(self) -> dict[str, Any]: """Return data of registry to save.""" self.save_calls += 1 - return None + return {} @pytest.mark.parametrize( diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 4f8c51c5397..729212f4c1d 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -58,9 +58,12 @@ async def test_caching_data(hass: HomeAssistant) -> None: # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE) - with patch( - "homeassistant.helpers.restore_state.Store.async_load", - side_effect=HomeAssistantError, + with ( + patch( + "homeassistant.helpers.restore_state.Store.async_load", + side_effect=HomeAssistantError, + ), + patch("homeassistant.helpers.restore_state.Store.async_save"), ): # Failure to load should not be treated as fatal await async_load(hass) @@ -68,7 +71,13 @@ async def test_caching_data(hass: HomeAssistant) -> None: data = async_get(hass) assert data.last_states == {} - await async_load(hass) + # Mock that only b1 is present this run + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await async_load(hass) + await hass.async_block_till_done() + data = async_get(hass) entity = RestoreEntity() @@ -76,11 +85,7 @@ async def test_caching_data(hass: HomeAssistant) -> None: entity.entity_id = "input_boolean.b1" # Mock that only b1 is present this run - with patch( - "homeassistant.helpers.restore_state.Store.async_save" - ) as mock_write_data: - state = await entity.async_get_last_state() - await hass.async_block_till_done() + state = await entity.async_get_last_state() assert state is not None assert state.entity_id == "input_boolean.b1" @@ -110,17 +115,17 @@ async def test_periodic_write(hass: HomeAssistant) -> None: await data.store.async_save([]) # Emulate a fresh load - hass.data.pop(DATA_RESTORE_STATE) - await async_load(hass) - data = async_get(hass) - - entity = RestoreEntity() - entity.hass = hass - entity.entity_id = "input_boolean.b1" - with patch( "homeassistant.helpers.restore_state.Store.async_save" ) as mock_write_data: + hass.data.pop(DATA_RESTORE_STATE) + await async_load(hass) + data = async_get(hass) + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + await entity.async_get_last_state() await hass.async_block_till_done() @@ -158,17 +163,17 @@ async def test_save_persistent_states(hass: HomeAssistant) -> None: await data.store.async_save([]) # Emulate a fresh load - hass.data.pop(DATA_RESTORE_STATE) - await async_load(hass) - data = async_get(hass) - - entity = RestoreEntity() - entity.hass = hass - entity.entity_id = "input_boolean.b1" - with patch( "homeassistant.helpers.restore_state.Store.async_save" ) as mock_write_data: + hass.data.pop(DATA_RESTORE_STATE) + await async_load(hass) + data = async_get(hass) + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + await entity.async_get_last_state() await hass.async_block_till_done() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 16db9fb7b05..3d662e772e8 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -16,7 +16,7 @@ import voluptuous as vol # Otherwise can't test just this file (import order issue) from homeassistant import config_entries, exceptions -import homeassistant.components.scene as scene +from homeassistant.components import scene from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -3322,7 +3322,9 @@ async def test_multiple_runs_repeat_choose( events = async_capture_events(hass, "abc") for _ in range(max_runs): - hass.async_create_task(script_obj.async_run(context=Context())) + hass.async_create_task( + script_obj.async_run(context=Context()), eager_start=False + ) await hass.async_block_till_done() assert "WARNING" not in caplog.text diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 74b8a86ce7c..c9d92c2f25a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -7,6 +7,7 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from pytest_unordered import unordered import voluptuous as vol # To prevent circular import when running just this file @@ -16,6 +17,7 @@ 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.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -785,7 +787,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: """Test async_get_all_descriptions.""" group_config = {DOMAIN_GROUP: {}} assert await async_setup_component(hass, DOMAIN_GROUP, group_config) - assert await async_setup_component(hass, "system_health", {}) + assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) with patch( "homeassistant.helpers.service._load_services_files", @@ -795,17 +797,19 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: # 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 proxy_load_services_files.mock_calls[0][1][1] == unordered( + [ + await async_get_integration(hass, DOMAIN_GROUP), + ] + ) assert len(descriptions) == 1 - - assert "description" in descriptions["group"]["reload"] - assert "fields" in descriptions["group"]["reload"] + assert DOMAIN_GROUP in descriptions + assert "description" in descriptions[DOMAIN_GROUP]["reload"] + assert "fields" in descriptions[DOMAIN_GROUP]["reload"] # Does not have services - assert "system_health" not in descriptions + assert DOMAIN_SYSTEM_HEALTH not in descriptions logger_config = {DOMAIN_LOGGER: {}} @@ -834,7 +838,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 2 - + assert DOMAIN_LOGGER in descriptions assert descriptions[DOMAIN_LOGGER]["set_default_level"]["name"] == "Translated name" assert ( descriptions[DOMAIN_LOGGER]["set_default_level"]["description"] @@ -1847,3 +1851,139 @@ async def test_async_extract_config_entry_ids(hass: HomeAssistant) -> None: ) assert await service.async_extract_config_entry_ids(hass, call) == {"abc"} + + +async def test_reload_service_helper(hass: HomeAssistant) -> None: + """Test the reload service helper.""" + + active_reload_calls = 0 + reloaded = [] + + async def reload_service_handler(service_call: ServiceCall) -> None: + """Remove all automations and load new ones from config.""" + nonlocal active_reload_calls + # Assert the reload helper prevents parallel reloads + assert not active_reload_calls + active_reload_calls += 1 + if not (target := service_call.data.get("target")): + reloaded.append("all") + else: + reloaded.append(target) + await asyncio.sleep(0.01) + active_reload_calls -= 1 + + def reload_targets(service_call: ServiceCall) -> set[str | None]: + if target_id := service_call.data.get("target"): + return {target_id} + return {"target1", "target2", "target3", "target4"} + + # Test redundant reload of single targets + reloader = service.ReloadServiceHelper(reload_service_handler, reload_targets) + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, target1) + # while the first task is reloaded, note that target1 can't be deduplicated + # because it's already being reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered( + ["target1", "target2", "target3", "target4", "target1"] + ) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, all) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test")), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["target1", "target2", "target3", "target4", "all"]) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (all) + reloader.execute_service(ServiceCall("test", "test")), + # These reload tasks will be deduplicated to (target1, target2, target3, target4) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["all", "target1", "target2", "target3", "target4"]) + + # Test redundant reload of single targets + reloaded.clear() + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, target1) + # while the first task is reloaded, note that target1 can't be deduplicated + # because it's already being reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered( + ["target1", "target2", "target3", "target4", "target1"] + ) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, all) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test")), + reloader.execute_service(ServiceCall("test", "test")), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["target1", "target2", "target3", "target4", "all"]) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (all) + reloader.execute_service(ServiceCall("test", "test")), + # These reload tasks will be deduplicated to (target1, target2, target3, target4) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["all", "target1", "target2", "target3", "target4"]) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 0d574e9811f..12dc56db85d 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -627,7 +627,7 @@ async def test_saving_load_round_trip(tmpdir: py.path.local) -> None: """Test saving and loading round trip.""" 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: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: class NamedTupleSubclass(NamedTuple): """A NamedTuple subclass.""" @@ -671,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(config_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: storage_key = "core.anything" store = storage.Store( hass, MOCK_VERSION_2, storage_key, minor_version=MOCK_MINOR_VERSION_1 @@ -730,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(config_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: hass.config.components.add("testdomain") storage_key = "testdomain.testkey" @@ -787,7 +787,7 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: """Test OSError during load is fatal.""" 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: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: store = storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -817,7 +817,7 @@ async def test_json_load_failure(tmpdir: py.path.local) -> None: """Test json load raising HomeAssistantError.""" 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: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: store = storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -883,7 +883,7 @@ async def test_store_manager_caching( config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) assert ( store_manager.async_fetch("integration1") is None @@ -957,7 +957,7 @@ async def test_store_manager_caching( await hass.async_stop(force=True) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) 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 @@ -992,7 +992,7 @@ async def test_store_manager_caching( # 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: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: integration1 = storage.Store(hass, 1, "integration1") assert await integration1.async_load() == {"integration1": "updated"} await integration1.async_save({"integration1": "updated2"}) @@ -1006,7 +1006,7 @@ async def test_store_manager_caching( await hass.async_stop(force=True) # Now remove the stores - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() await store_manager.async_preload(["integration1", "integration2"]) @@ -1031,7 +1031,7 @@ async def test_store_manager_caching( 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: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() await store_manager.async_preload(["integration1"]) @@ -1058,7 +1058,7 @@ async def test_store_manager_sub_dirs(tmpdir: py.path.local) -> None: config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() assert store_manager.async_fetch("subdir/integration1") is None @@ -1087,7 +1087,7 @@ async def test_store_manager_cleanup_after_started( config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: hass.set_state(CoreState.not_running) store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() @@ -1137,7 +1137,7 @@ async def test_store_manager_cleanup_after_stop( config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: hass.set_state(CoreState.not_running) store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index b6dc1616a48..da436d799aa 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -7,7 +7,7 @@ import pytest from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant -import homeassistant.helpers.sun as sun +from homeassistant.helpers import sun import homeassistant.util.dt as dt_util diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 54fdf0368eb..1e2e512cf3d 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1176,7 +1176,7 @@ def test_as_datetime(hass: HomeAssistant, input) -> None: ) def test_as_datetime_from_timestamp( hass: HomeAssistant, - input: int | float, + input: float, output: str, ) -> None: """Test converting a UNIX timestamp to a date object.""" @@ -1198,6 +1198,35 @@ def test_as_datetime_from_timestamp( ) +@pytest.mark.parametrize( + ("input", "output"), + [ + ( + "{% set dt = as_datetime('2024-01-01 16:00:00-08:00') %}", + "2024-01-01 16:00:00-08:00", + ), + ( + "{% set dt = as_datetime('2024-01-29').date() %}", + "2024-01-29 00:00:00", + ), + ], +) +def test_as_datetime_from_datetime( + hass: HomeAssistant, input: str, output: str +) -> None: + """Test using datetime.datetime or datetime.date objects as input.""" + + assert ( + template.Template(f"{input}{{{{ dt | as_datetime }}}}", hass).async_render() + == output + ) + + assert ( + template.Template(f"{input}{{{{ as_datetime(dt) }}}}", hass).async_render() + == output + ) + + @pytest.mark.parametrize( ("input", "default", "output"), [ @@ -1270,7 +1299,7 @@ def test_to_json(hass: HomeAssistant) -> None: # Test special case where substring class cannot be rendered # See: https://github.com/ijl/orjson/issues/445 class MyStr(str): - pass + __slots__ = () expected_result = '{"mykey1":11.0,"mykey2":"myvalue2","mykey3":["opt3b","opt3a"]}' test_dict = { @@ -2050,12 +2079,7 @@ async def test_state_translated( ): if category == "entity": return { - "component.hue.entity.light.translation_key.state.on": "state_is_on" - } - if category == "state": - return { - "component.some_domain.state.some_device_class.off": "state_is_off", - "component.some_domain.state._.foo": "state_is_foo", + "component.hue.entity.light.translation_key.state.on": "state_is_on", } return {} @@ -2066,16 +2090,6 @@ async def test_state_translated( tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass) assert tpl8.async_render() == "state_is_on" - tpl9 = template.Template( - '{{ state_translated("some_domain.with_device_class_1") }}', hass - ) - assert tpl9.async_render() == "state_is_off" - - tpl10 = template.Template( - '{{ state_translated("some_domain.with_device_class_2") }}', hass - ) - assert tpl10.async_render() == "state_is_foo" - tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass) assert tpl11.async_render() == "unavailable" @@ -2235,7 +2249,6 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: hass, ).async_render() assert result == "1 hour" - result = template.Template( ( "{{" @@ -2290,10 +2303,369 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: ).async_render() assert result == "string" + # Test behavior when current time is same as the input time + result = template.Template( + ( + "{{" + " relative_time(" + " strptime(" + ' "2000-01-01 10:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "0 seconds" + + # Test behavior when the input time is in the future + result = template.Template( + ( + "{{" + " relative_time(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2000-01-01 11:00:00+00:00" + info = template.Template(relative_time_template, hass).async_render_to_info() assert info.has_time is True +@patch( + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", + return_value=True, +) +def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: + """Test time_since method.""" + hass.config.set_time_zone("UTC") + now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + time_since_template = ( + '{{time_since(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' + ) + with freeze_time(now): + result = template.Template( + time_since_template, + hass, + ).async_render() + assert result == "1 hour" + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 09:00:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 03:00:00 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour" + + result1 = str( + template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 2" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 09:05:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=2" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 55 minutes" + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 3" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z")' + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "1999-02-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 0" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "11 months 4 days 1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "1999-02-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z")' + " )" + "}}" + ), + hass, + ).async_render() + assert result == "11 months" + result1 = str( + template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=3" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + '{{time_since("string")}}', + hass, + ).async_render() + assert result == "string" + + info = template.Template(time_since_template, hass).async_render_to_info() + assert info.has_time is True + + +@patch( + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", + return_value=True, +) +def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: + """Test time_until method.""" + hass.config.set_time_zone("UTC") + now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + time_until_template = ( + '{{time_until(strptime("2000-01-01 11:00:00", "%Y-%m-%d %H:%M:%S"))}}' + ) + with freeze_time(now): + result = template.Template( + time_until_template, + hass, + ).async_render() + assert result == "1 hour" + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 13:00:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 05:00:00 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour" + + result1 = str( + template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 09:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 2" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 12:05:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=2" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 5 minutes" + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 3" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z")' + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2001-02-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 0" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 year 1 month 2 days 1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2001-02-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 4" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 year 1 month 2 days 2 hours" + result1 = str( + template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 09:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=3" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + '{{time_until("string")}}', + hass, + ).async_render() + assert result == "string" + + info = template.Template(time_until_template, hass).async_render_to_info() + assert info.has_time is True + + @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, @@ -5743,3 +6115,20 @@ async def test_label_areas( info = render_to_info(hass, f"{{{{ '{label.name}' | label_areas }}}}") assert_result_info(info, [master_bedroom.id]) assert info.rate_limit is None + + +async def test_template_thread_safety_checks(hass: HomeAssistant) -> None: + """Test template thread safety checks.""" + hass.states.async_set("sensor.test", "23") + template_str = "{{ states('sensor.test') }}" + template_obj = template.Template(template_str, None) + template_obj.hass = hass + hass.config.debug = True + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_render_to_info from a thread.", + ): + await hass.async_add_executor_job(template_obj.async_render_to_info) + + assert template_obj.async_render_to_info().result() == 23 diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 1bba23c51a1..b841e1ab5ac 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -47,35 +47,10 @@ async def test_component_translation_path( {"switch": [{"platform": "test"}, {"platform": "test_embedded"}]}, ) assert await async_setup_component(hass, "test_package", {"test_package": None}) - - ( - int_test, - int_test_embedded, - int_test_package, - ) = await asyncio.gather( - async_get_integration(hass, "test"), - async_get_integration(hass, "test_embedded"), - async_get_integration(hass, "test_package"), - ) + int_test_package = await async_get_integration(hass, "test_package") assert path.normpath( - translation.component_translation_path("test.switch", "en", int_test) - ) == path.normpath( - hass.config.path("custom_components", "test", "translations", "switch.en.json") - ) - - assert path.normpath( - translation.component_translation_path( - "test_embedded.switch", "en", int_test_embedded - ) - ) == path.normpath( - hass.config.path( - "custom_components", "test_embedded", "translations", "switch.en.json" - ) - ) - - assert path.normpath( - translation.component_translation_path("test_package", "en", int_test_package) + translation.component_translation_path("en", int_test_package) ) == path.normpath( hass.config.path("custom_components", "test_package", "translations", "en.json") ) @@ -86,28 +61,39 @@ def test__load_translations_files_by_language( ) -> None: """Test the load translation files function.""" # Test one valid and one invalid file - file1 = hass.config.path( - "custom_components", "test", "translations", "switch.en.json" - ) - file2 = hass.config.path( + en_file = hass.config.path("custom_components", "test", "translations", "en.json") + invalid_file = hass.config.path( "custom_components", "test", "translations", "invalid.json" ) - file3 = hass.config.path( - "custom_components", "test", "translations", "_broken.en.json" + broken_file = hass.config.path( + "custom_components", "test", "translations", "_broken.json" ) assert translation._load_translations_files_by_language( - {"en": {"switch.test": file1, "invalid": file2, "broken": file3}} - ) == { - "en": { - "switch.test": { - "state": {"string1": "Value 1", "string2": "Value 2"}, - "something": "else", - }, - "invalid": {}, + { + "en": {"test": en_file}, + "invalid": {"test": invalid_file}, + "broken": {"test": broken_file}, } + ) == { + "broken": {}, + "en": { + "test": { + "entity": { + "switch": { + "other1": {"name": "Other 1"}, + "other2": {"name": "Other 2"}, + "other3": {"name": "Other 3"}, + "other4": {"name": "Other 4"}, + "outlet": {"name": "Outlet " "{placeholder}"}, + } + }, + "something": "else", + } + }, + "invalid": {"test": {}}, } assert "Translation file is unexpected type" in caplog.text - assert "_broken.en.json" in caplog.text + assert "_broken.json" in caplog.text @pytest.mark.parametrize( @@ -185,33 +171,61 @@ async def test_get_translations( hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None ) -> None: """Test the get translations helper.""" - translations = await translation.async_get_translations(hass, "en", "state") + translations = await translation.async_get_translations(hass, "en", "entity") assert translations == {} assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) await hass.async_block_till_done() - translations = await translation.async_get_translations(hass, "en", "state") + translations = await translation.async_get_translations( + hass, "en", "entity", {"test"} + ) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } - translations = await translation.async_get_translations(hass, "de", "state") - assert "component.switch.something" not in translations - assert translations["component.switch.state.string1"] == "German Value 1" - assert translations["component.switch.state.string2"] == "German Value 2" + translations = await translation.async_get_translations( + hass, "de", "entity", {"test"} + ) + + assert translations == { + "component.test.entity.switch.other1.name": "Anderes 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } # Test a partial translation - translations = await translation.async_get_translations(hass, "es", "state") - assert translations["component.switch.state.string1"] == "Spanish Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + translations = await translation.async_get_translations( + hass, "es", "entity", {"test"} + ) + + assert translations == { + "component.test.entity.switch.other1.name": "Otra 1", + "component.test.entity.switch.other2.name": "Otra 2", + "component.test.entity.switch.other3.name": "Otra 3", + "component.test.entity.switch.other4.name": "Otra 4", + "component.test.entity.switch.outlet.name": "Enchufe {placeholder}", + } # Test that an untranslated language falls back to English. translations = await translation.async_get_translations( - hass, "invalid-language", "state" + hass, "invalid-language", "entity", {"test"} ) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } async def test_get_translations_loads_config_flows( @@ -348,162 +362,6 @@ async def test_get_translation_categories(hass: HomeAssistant) -> None: assert "component.light.device_automation.action_type.turn_on" in translations -async def test_legacy_platform_translations_not_used_built_in_integrations( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """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"]["test_legacy_state_translations.sensor"] = { - "state": { - "test_legacy_state_translations__phase": { - "first_quarter": "First Quarter" - } - } - } - return result - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - 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"]["test_legacy_state_translations_bad_data.sensor"] = { - "state": "bad data" - } - return result - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_bad_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - assert ( - "An integration providing translations for sensor provided invalid data:" - " bad data" - ) in caplog.text - - -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. - - 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"]["test_legacy_state_translations.sensor"] = { - "state": { - "test_legacy_state_translations__phase": { - "first_quarter": "First Quarter" - } - } - } - return result - - hass.config.components.add("sensor") - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - not in translations - ) - - hass.config.components.add("test_legacy_state_translations.sensor") - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations( - hass, "en", "state", integrations={"sensor"} - ) - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - async def test_translation_merging_loaded_together( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -592,14 +450,14 @@ async def test_caching(hass: HomeAssistant) -> None: # Patch with same method so we can count invocations with patch( - "homeassistant.helpers.translation._merge_resources", - side_effect=translation._merge_resources, - ) as mock_merge: + "homeassistant.helpers.translation.build_resources", + side_effect=translation.build_resources, + ) as mock_build_resources: load1 = await translation.async_get_translations(hass, "en", "entity_component") - assert len(mock_merge.mock_calls) == 1 + assert len(mock_build_resources.mock_calls) == 5 load2 = await translation.async_get_translations(hass, "en", "entity_component") - assert len(mock_merge.mock_calls) == 1 + assert len(mock_build_resources.mock_calls) == 5 assert load1 == load2 @@ -665,47 +523,58 @@ async def test_custom_component_translations( async def test_get_cached_translations( hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None -): +) -> None: """Test the get cached translations helper.""" - translations = translation.async_get_cached_translations(hass, "en", "state") + translations = await translation.async_get_translations(hass, "en", "entity") assert translations == {} assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) await hass.async_block_till_done() - await translation._async_get_translations_cache(hass).async_load( - "en", hass.config.components - ) - translations = translation.async_get_cached_translations(hass, "en", "state") + await translation._async_get_translations_cache(hass).async_load("en", {"test"}) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" - - await translation._async_get_translations_cache(hass).async_load( - "de", hass.config.components + translations = translation.async_get_cached_translations( + hass, "en", "entity", "test" ) - translations = translation.async_get_cached_translations(hass, "de", "state") - assert "component.switch.something" not in translations - assert translations["component.switch.state.string1"] == "German Value 1" - assert translations["component.switch.state.string2"] == "German Value 2" + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } + + await translation._async_get_translations_cache(hass).async_load("es", {"test"}) # Test a partial translation - await translation._async_get_translations_cache(hass).async_load( - "es", hass.config.components + translations = translation.async_get_cached_translations( + hass, "es", "entity", "test" + ) + + assert translations == { + "component.test.entity.switch.other1.name": "Otra 1", + "component.test.entity.switch.other2.name": "Otra 2", + "component.test.entity.switch.other3.name": "Otra 3", + "component.test.entity.switch.other4.name": "Otra 4", + "component.test.entity.switch.outlet.name": "Enchufe {placeholder}", + } + + await translation._async_get_translations_cache(hass).async_load( + "invalid-language", {"test"} ) - translations = translation.async_get_cached_translations(hass, "es", "state") - assert translations["component.switch.state.string1"] == "Spanish Value 1" - assert translations["component.switch.state.string2"] == "Value 2" # Test that an untranslated language falls back to English. - await translation._async_get_translations_cache(hass).async_load( - "invalid-language", hass.config.components - ) translations = translation.async_get_cached_translations( - hass, "invalid-language", "state" + hass, "invalid-language", "entity", "test" ) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } async def test_setup(hass: HomeAssistant): @@ -784,36 +653,6 @@ async def test_translate_state(hass: HomeAssistant): mock.assert_called_once_with(hass, hass.config.language, "entity_component") assert result == "TRANSLATED" - with patch( - "homeassistant.helpers.translation.async_get_cached_translations", - return_value={"component.binary_sensor.state.device_class.on": "TRANSLATED"}, - ) as mock: - result = translation.async_translate_state( - hass, "on", "binary_sensor", "platform", None, "device_class" - ) - mock.assert_has_calls( - [ - call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), - ] - ) - assert result == "TRANSLATED" - - with patch( - "homeassistant.helpers.translation.async_get_cached_translations", - return_value={"component.binary_sensor.state._.on": "TRANSLATED"}, - ) as mock: - result = translation.async_translate_state( - hass, "on", "binary_sensor", "platform", None, None - ) - mock.assert_has_calls( - [ - call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), - ] - ) - assert result == "TRANSLATED" - with patch( "homeassistant.helpers.translation.async_get_cached_translations", return_value={}, @@ -824,7 +663,6 @@ async def test_translate_state(hass: HomeAssistant): mock.assert_has_calls( [ call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), ] ) assert result == "on" @@ -840,7 +678,6 @@ async def test_translate_state(hass: HomeAssistant): [ call(hass, hass.config.language, "entity"), call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), ] ) assert result == "on" diff --git a/tests/helpers/test_typing.py b/tests/helpers/test_typing.py new file mode 100644 index 00000000000..5b50a8864de --- /dev/null +++ b/tests/helpers/test_typing.py @@ -0,0 +1,37 @@ +"""Test typing helper module.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from homeassistant.core import Context, Event, HomeAssistant, ServiceCall +from homeassistant.helpers import typing as ha_typing + +from tests.common import import_and_test_deprecated_alias + + +@pytest.mark.parametrize( + ("alias_name", "replacement", "breaks_in_ha_version"), + [ + ("ContextType", Context, "2025.5"), + ("EventType", Event, "2025.5"), + ("HomeAssistantType", HomeAssistant, "2025.5"), + ("ServiceCallType", ServiceCall, "2025.5"), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Test deprecated aliases.""" + import_and_test_deprecated_alias( + caplog, + ha_typing, + alias_name, + replacement, + breaks_in_ha_version, + ) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 775dc08f1d4..8633bf862a5 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -63,14 +63,13 @@ def get_crd( calls += 1 return calls - crd = update_coordinator.DataUpdateCoordinator[int]( + return update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", update_method=refresh, update_interval=update_interval, ) - return crd DEFAULT_UPDATE_INTERVAL = timedelta(seconds=10) diff --git a/tests/non_packaged_scripts/__init__.py b/tests/non_packaged_scripts/__init__.py new file mode 100644 index 00000000000..852c52a8293 --- /dev/null +++ b/tests/non_packaged_scripts/__init__.py @@ -0,0 +1 @@ +"""Tests for the non-packaged scripts in the script directory.""" diff --git a/tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr b/tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr new file mode 100644 index 00000000000..bad47eedf53 --- /dev/null +++ b/tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr @@ -0,0 +1,62 @@ +# serializer version: 1 +# name: test_alexa_locales + ''' + Missing interfaces: + ['Alexa.ApplicationStateReporter', + 'Alexa.AuthorizationController', + 'Alexa.AutomationManagement', + 'Alexa.Commissionable', + 'Alexa.Cooking', + 'Alexa.DataController', + 'Alexa.InventoryLevelSensor', + 'Alexa.InventoryLevelUsageSensor', + 'Alexa.InventoryUsageSensor', + 'Alexa.KeypadController', + 'Alexa.Launcher', + 'Alexa.PercentageController', + 'Alexa.ProactiveNotificationSource', + 'Alexa.RecordController', + 'Alexa.RemoteVideoPlayer', + 'Alexa.RTCSessionController', + 'Alexa.SimpleEventSource', + 'Alexa.UIController', + 'Alexa.UserPreference', + 'Alexa.VideoRecorder', + 'Alexa.WakeOnLANController'] + + + Interfaces where upstream locales are not subsets of the core locales: + [] + + + Interfaces checked ok: + ['AlexaBrightnessController', + 'AlexaCameraStreamController', + 'AlexaChannelController', + 'AlexaColorController', + 'AlexaColorTemperatureController', + 'AlexaContactSensor', + 'AlexaDoorbellEventSource', + 'AlexaEndpointHealth', + 'AlexaEqualizerController', + 'AlexaInputController', + 'AlexaLockController', + 'AlexaModeController', + 'AlexaMotionSensor', + 'AlexaPlaybackController', + 'AlexaPlaybackStateReporter', + 'AlexaPowerController', + 'AlexaPowerLevelController', + 'AlexaRangeController', + 'AlexaSceneController', + 'AlexaSecurityPanelController', + 'AlexaSeekController', + 'AlexaSpeaker', + 'AlexaStepSpeaker', + 'AlexaTemperatureSensor', + 'AlexaThermostatController', + 'AlexaTimeHoldController', + 'AlexaToggleController'] + + ''' +# --- diff --git a/tests/non_packaged_scripts/test_alexa_locales.py b/tests/non_packaged_scripts/test_alexa_locales.py new file mode 100644 index 00000000000..ea139f7de8e --- /dev/null +++ b/tests/non_packaged_scripts/test_alexa_locales.py @@ -0,0 +1,29 @@ +"""Test the alexa_locales script.""" + +from pathlib import Path + +import pytest +import requests_mock +from syrupy import SnapshotAssertion + +from script.alexa_locales import SITE, run_script + + +def test_alexa_locales( + capsys: pytest.CaptureFixture[str], + requests_mock: requests_mock.Mocker, + snapshot: SnapshotAssertion, +) -> None: + """Test alexa_locales script.""" + fixture_file = ( + Path(__file__).parent.parent / "fixtures/non_packaged_scripts/alexa_locales.txt" + ) + requests_mock.get( + SITE, + text=fixture_file.read_text(encoding="utf-8"), + ) + + run_script() + + captured = capsys.readouterr() + assert captured.out == snapshot diff --git a/tests/ruff.toml b/tests/ruff.toml index 76e4feacdd2..87725160751 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -2,24 +2,11 @@ extend = "../pyproject.toml" [lint] -extend-select = [ - "PT001", # Use @pytest.fixture without parentheses - "PT002", # Configuration for fixture specified via positional args, use kwargs - "PT003", # The scope='function' is implied in @pytest.fixture() - "PT006", # Single parameter in parameterize is a string, multiple a tuple - "PT013", # Found incorrect pytest import, use simple import pytest instead - "PT015", # Assertion always fails, replace with pytest.fail() - "PT021", # use yield instead of request.addfinalizer - "PT022", # No teardown in fixture, replace useless yield with return -] extend-ignore = [ - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint "B904", # Use raise from to specify exception cause "N815", # Variable {name} in class scope should not be mixedCase + "RUF018", # Avoid assignment expressions in assert statements ] [lint.isort] diff --git a/tests/script/__init__.py b/tests/script/__init__.py new file mode 100644 index 00000000000..209299782c9 --- /dev/null +++ b/tests/script/__init__.py @@ -0,0 +1 @@ +"""Tests for scripts.""" diff --git a/tests/script/test_gen_requirements_all.py b/tests/script/test_gen_requirements_all.py new file mode 100644 index 00000000000..793b3de63c5 --- /dev/null +++ b/tests/script/test_gen_requirements_all.py @@ -0,0 +1,25 @@ +"""Tests for the gen_requirements_all script.""" + +from script import gen_requirements_all + + +def test_overrides_normalized() -> None: + """Test override lists are using normalized package names.""" + for req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL: + assert req == gen_requirements_all._normalize_package_name(req) + for req in gen_requirements_all.INCLUDED_REQUIREMENTS_WHEELS: + assert req == gen_requirements_all._normalize_package_name(req) + for overrides in gen_requirements_all.OVERRIDDEN_REQUIREMENTS_ACTIONS.values(): + for req in overrides["exclude"]: + assert req == gen_requirements_all._normalize_package_name(req) + for req in overrides["include"]: + assert req == gen_requirements_all._normalize_package_name(req) + + +def test_include_overrides_subsets() -> None: + """Test packages in include override lists are present in the exclude list.""" + for req in gen_requirements_all.INCLUDED_REQUIREMENTS_WHEELS: + assert req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL + for overrides in gen_requirements_all.OVERRIDDEN_REQUIREMENTS_ACTIONS.values(): + for req in overrides["include"]: + assert req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 8367eda76e8..72bb4dd5b67 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -42,9 +42,7 @@ async def test_list_user(hass: HomeAssistant, provider, capsys) -> None: captured = capsys.readouterr() - assert captured.out == "\n".join( - ["test-user", "second-user", "", "Total users: 2", ""] - ) + assert captured.out == "test-user\nsecond-user\n\nTotal users: 2\n" async def test_add_user( diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index a77e5bf504a..79c64259f8b 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.config import YAML_CONFIG_FILE -import homeassistant.scripts.check_config as check_config +from homeassistant.scripts import check_config from tests.common import get_test_config_dir @@ -135,6 +135,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None: "server_port": 8123, "ssl_profile": "modern", "use_x_frame_options": True, + "server_host": ["0.0.0.0", "::"], } assert res["secret_cache"] == { get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"} diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 44edece1812..002ade2baef 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch -import homeassistant.scripts as scripts +from homeassistant import scripts @patch("homeassistant.scripts.get_default_config_dir", return_value="/default") diff --git a/tests/test_backports.py b/tests/test_backports.py new file mode 100644 index 00000000000..09c11da37cb --- /dev/null +++ b/tests/test_backports.py @@ -0,0 +1,41 @@ +"""Test backports package.""" + +from __future__ import annotations + +from enum import StrEnum +from functools import cached_property +from types import ModuleType +from typing import Any + +import pytest + +from homeassistant.backports import ( + enum as backports_enum, + functools as backports_functools, +) + +from tests.common import import_and_test_deprecated_alias + + +@pytest.mark.parametrize( + ("module", "replacement", "breaks_in_ha_version"), + [ + (backports_enum, StrEnum, "2025.5"), + (backports_functools, cached_property, "2025.5"), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Test deprecated aliases.""" + alias_name = replacement.__name__ + import_and_test_deprecated_alias( + caplog, + module, + alias_name, + replacement, + breaks_in_ha_version, + ) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py new file mode 100644 index 00000000000..688852ecf55 --- /dev/null +++ b/tests/test_block_async_io.py @@ -0,0 +1,200 @@ +"""Tests for async util methods from Python source.""" + +import importlib +import time +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import block_async_io + +from tests.common import extract_stack_to_frame + + +async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> None: + """Test time.sleep injected by the debugger is not reported.""" + block_async_io.enable() + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + time.sleep(0) + assert "Detected blocking call inside the event loop" not in caplog.text + + +async def test_protect_loop_sleep(caplog: pytest.LogCaptureFixture) -> None: + """Test time.sleep not injected by the debugger raises.""" + block_async_io.enable() + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + pytest.raises( + RuntimeError, match="Detected blocking call to sleep inside the event loop" + ), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + time.sleep(0) + + +async def test_protect_loop_sleep_get_current_frame_raises( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test time.sleep when get_current_frame raises ValueError.""" + block_async_io.enable() + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + pytest.raises( + RuntimeError, match="Detected blocking call to sleep inside the event loop" + ), + patch( + "homeassistant.block_async_io.get_current_frame", + side_effect=ValueError, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + time.sleep(0) + + +async def test_protect_loop_importlib_import_module_non_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test import_module in the loop for non-loaded module.""" + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + pytest.raises(ImportError), + patch.object(block_async_io, "_IN_TESTS", False), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + block_async_io.enable() + importlib.import_module("not_loaded_module") + + assert "Detected blocking call to import_module" in caplog.text + + +async def test_protect_loop_importlib_import_loaded_module_non_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test import_module in the loop for a loaded module.""" + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + patch.object(block_async_io, "_IN_TESTS", False), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + block_async_io.enable() + importlib.import_module("sys") + + assert "Detected blocking call to import_module" not in caplog.text + + +async def test_protect_loop_importlib_import_module_in_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test import_module in the loop for non-loaded module in an integration.""" + frames = 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()", + ), + ] + ) + with ( + pytest.raises(ImportError), + patch.object(block_async_io, "_IN_TESTS", False), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + block_async_io.enable() + importlib.import_module("not_loaded_module") + + assert ( + "Detected blocking call to import_module inside the event loop by " + "integration 'hue' at homeassistant/components/hue/light.py, line 23" + ) in caplog.text diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index de82aba9911..3d2735d9c1c 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -13,7 +13,7 @@ import pytest 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.const import CONF_DEBUG, SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import CoreState, HomeAssistant, async_get_hass, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -44,7 +44,7 @@ async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped.""" -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="module", autouse=True) def mock_http_start_stop() -> Generator[None, None, None]: """Mock HTTP start and stop.""" with ( @@ -112,6 +112,48 @@ async def test_empty_setup(hass: HomeAssistant) -> None: assert domain in hass.config.components, domain +@pytest.mark.parametrize("load_registries", [False]) +async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: + """Test that config does not turn off debug if its turned on by runtime config.""" + # Mock that its turned on from RuntimeConfig + hass.config.debug = True + + await bootstrap.async_from_config_dict({CONF_DEBUG: False}, hass) + assert hass.config.debug is True + + +@pytest.mark.parametrize("hass_config", [{"frontend": {}}]) +async def test_asyncio_debug_on_turns_hass_debug_on( + mock_hass_config: None, + mock_enable_logging: Mock, + mock_is_virtual_env: Mock, + mock_mount_local_lib_path: AsyncMock, + mock_ensure_config_exists: AsyncMock, + mock_process_ha_config_upgrade: Mock, +) -> None: + """Test that asyncio debug turns on hass debug.""" + asyncio.get_running_loop().set_debug(True) + + verbose = Mock() + log_rotate_days = Mock() + log_file = Mock() + log_no_color = Mock() + + hass = await bootstrap.async_setup_hass( + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + recovery_mode=False, + ), + ) + + assert hass.config.debug is True + + @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.""" @@ -599,6 +641,7 @@ async def test_setup_hass( log_no_color=log_no_color, skip_pip=True, recovery_mode=False, + debug=True, ), ) @@ -619,6 +662,9 @@ async def test_setup_hass( assert len(mock_ensure_config_exists.mock_calls) == 1 assert len(mock_process_ha_config_upgrade.mock_calls) == 1 + # debug in RuntimeConfig should set it it in hass.config + assert hass.config.debug is True + assert hass == async_get_hass() @@ -1097,16 +1143,11 @@ async def test_bootstrap_empty_integrations( await hass.async_block_till_done() -@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"]) -@pytest.mark.parametrize("load_registries", [False]) -async def test_bootstrap_dependencies( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - integration: str, -) -> None: - """Test dependencies are set up correctly,.""" +@pytest.fixture(name="mock_mqtt_config_flow") +def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: + """Mock MQTT config flow.""" + original_mqtt = HANDLERS.get("mqtt") - # Prepare MQTT config entry @HANDLERS.register("mqtt") class MockConfigFlow: """Mock the MQTT config flow.""" @@ -1114,6 +1155,22 @@ async def test_bootstrap_dependencies( VERSION = 1 MINOR_VERSION = 1 + yield + if original_mqtt: + HANDLERS["mqtt"] = original_mqtt + else: + HANDLERS.pop("mqtt") + + +@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"]) +@pytest.mark.parametrize("load_registries", [False]) +async def test_bootstrap_dependencies( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + integration: str, + mock_mqtt_config_flow: None, +) -> None: + """Test dependencies are set up correctly,.""" entry = MockConfigEntry(domain="mqtt", data={"broker": "test-broker"}) entry.add_to_hass(hass) @@ -1398,3 +1455,13 @@ async def test_setup_does_base_platforms_first(hass: HomeAssistant) -> None: # only that they are setup before other integrations. assert set(order[1:3]) == {"sensor", "binary_sensor"} assert order[3:] == ["root", "first_dep", "second_dep"] + + +def test_should_rollover_is_always_false(): + """Test that shouldRollover always returns False.""" + assert ( + bootstrap._RotatingFileHandlerWithoutShouldRollOver( + "any.log", delay=True + ).shouldRollover(Mock()) + is False + ) diff --git a/tests/test_config.py b/tests/test_config.py index c20e2822592..58529fb0057 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -39,8 +39,11 @@ from homeassistant.core import ( 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 import ( + check_config, + config_validation as cv, + issue_registry as ir, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, async_get_integration @@ -378,7 +381,6 @@ 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" @@ -855,6 +857,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: "internal_url": "http://example.local", "media_dirs": {"mymedia": "/usr"}, "legacy_templates": True, + "debug": True, "currency": "EUR", "country": "SE", "language": "sv", @@ -875,6 +878,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: assert hass.config.media_dirs == {"mymedia": "/usr"} assert hass.config.config_source is ConfigSource.YAML assert hass.config.legacy_templates is True + assert hass.config.debug is True assert hass.config.currency == "EUR" assert hass.config.country == "SE" assert hass.config.language == "sv" @@ -2039,12 +2043,12 @@ async def test_core_config_schema_legacy_template( await config_util.async_process_ha_core_config(hass, config) issue_registry = ir.async_get(hass) - for issue_id in {"legacy_templates_true", "legacy_templates_false"}: + for issue_id in ("legacy_templates_true", "legacy_templates_false"): issue = issue_registry.async_get_issue("homeassistant", issue_id) assert issue if issue_id == expected_issue else not issue await config_util.async_process_ha_core_config(hass, {}) - for issue_id in {"legacy_templates_true", "legacy_templates_false"}: + for issue_id in ("legacy_templates_true", "legacy_templates_false"): assert not issue_registry.async_get_issue("homeassistant", issue_id) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 7d564f1cf12..8d7efad8918 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Generator from datetime import timedelta +from functools import cached_property import logging from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch @@ -14,7 +15,6 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.backports.functools import cached_property from homeassistant.components import dhcp from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import ( @@ -803,7 +803,7 @@ async def test_saving_and_loading( # Ensure same order for orig, loaded in zip( - hass.config_entries.async_entries(), manager.async_entries() + hass.config_entries.async_entries(), manager.async_entries(), strict=False ): assert orig.as_dict() == loaded.as_dict() @@ -821,9 +821,11 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: "_setup_lock", "update_listeners", "reason", + "error_reason_translation_key", + "error_reason_translation_placeholders", "_async_cancel_retry_setup", "_on_unload", - "reload_lock", + "setup_lock", "_reauth_lock", "_tasks", "_background_tasks", @@ -1282,6 +1284,53 @@ async def test_setup_does_not_retry_during_shutdown(hass: HomeAssistant) -> None assert len(mock_setup_entry.mock_calls) == 1 +async def test_reload_during_setup_retrying_waits(hass: HomeAssistant) -> None: + """Test reloading during setup retry waits.""" + entry = MockConfigEntry(domain="test") + entry.add_to_hass(hass) + load_attempts = [] + sleep_duration = 0 + + async def _mock_setup_entry(hass, entry): + """Mock setup entry.""" + nonlocal sleep_duration + await asyncio.sleep(sleep_duration) + load_attempts.append(entry.entry_id) + raise ConfigEntryNotReady + + mock_integration(hass, MockModule("test", async_setup_entry=_mock_setup_entry)) + mock_platform(hass, "test.config_flow", None) + + await hass.async_create_task( + hass.config_entries.async_setup(entry.entry_id), eager_start=True + ) + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + + # Now make the setup take a while so that the setup retry + # will still be in progress when the reload request comes in + sleep_duration = 0.1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await asyncio.sleep(0) + + # Should not raise homeassistant.config_entries.OperationNotAllowed + await hass.config_entries.async_reload(entry.entry_id) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await asyncio.sleep(0) + + # Should not raise homeassistant.config_entries.OperationNotAllowed + hass.config_entries.async_schedule_reload(entry.entry_id) + await hass.async_block_till_done() + + assert load_attempts == [ + entry.entry_id, + entry.entry_id, + entry.entry_id, + entry.entry_id, + entry.entry_id, + ] + + async def test_create_entry_options( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: @@ -1583,7 +1632,6 @@ async def test_entry_reload_succeed( mock_platform(hass, "comp.config_flow", None) assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 assert entry.state is config_entries.ConfigEntryState.LOADED @@ -1658,6 +1706,8 @@ async def test_entry_reload_error( ), ) + hass.config.components.add("comp") + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_reload(entry.entry_id) @@ -1689,8 +1739,11 @@ async def test_entry_disable_succeed( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") # Disable + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 assert await manager.async_set_disabled_by( entry.entry_id, config_entries.ConfigEntryDisabler.USER ) @@ -1702,7 +1755,7 @@ async def test_entry_disable_succeed( # Enable assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_unload_entry.mock_calls) == 1 - assert len(async_setup.mock_calls) == 1 + assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 1 assert entry.state is config_entries.ConfigEntryState.LOADED @@ -1726,6 +1779,7 @@ async def test_entry_disable_without_reload_support( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") # Disable assert not await manager.async_set_disabled_by( @@ -1902,7 +1956,7 @@ async def test_reload_entry_entity_registry_works( ) await hass.async_block_till_done() - assert len(mock_unload_entry.mock_calls) == 2 + assert len(mock_unload_entry.mock_calls) == 1 async def test_unique_id_persisted( @@ -2639,10 +2693,6 @@ async def test_unignore_step_form( await manager.async_remove(entry.entry_id) - # Right after removal there shouldn't be an entry or active flows - assert len(hass.config_entries.async_entries("comp")) == 0 - assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 - # But after a 'tick' the unignore step has run and we can see an active flow again. await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 1 @@ -2686,10 +2736,6 @@ async def test_unignore_create_entry( await manager.async_remove(entry.entry_id) - # Right after removal there shouldn't be an entry or flow - assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 - assert len(hass.config_entries.async_entries("comp")) == 0 - # But after a 'tick' the unignore step has run and we can see a config entry. await hass.async_block_till_done() entry = hass.config_entries.async_entries("comp")[0] @@ -3351,6 +3397,7 @@ async def test_entry_reload_calls_on_unload_listeners( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") mock_unload_callback = Mock() @@ -3903,8 +3950,9 @@ async def test_deprecated_disabled_by_str_set( caplog: pytest.LogCaptureFixture, ) -> None: """Test deprecated str set disabled_by enumizes and logs a warning.""" - entry = MockConfigEntry() + entry = MockConfigEntry(domain="comp") entry.add_to_manager(manager) + hass.config.components.add("comp") assert await manager.async_set_disabled_by( entry.entry_id, config_entries.ConfigEntryDisabler.USER.value ) @@ -3922,6 +3970,47 @@ async def test_entry_reload_concurrency( async_setup = AsyncMock(return_value=True) loaded = 1 + async def _async_setup_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded += 1 + return loaded == 1 + + async def _async_unload_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded -= 1 + return loaded == 0 + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=_async_setup_entry, + async_unload_entry=_async_unload_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") + 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 + + +async def test_entry_reload_concurrency_not_setup_setup( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test multiple reload calls do not cause a reload race.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + loaded = 0 + async def _async_setup_entry(*args, **kwargs): await asyncio.sleep(0) nonlocal loaded @@ -4033,6 +4122,7 @@ async def test_disallow_entry_reload_with_setup_in_progress( domain="comp", state=config_entries.ConfigEntryState.SETUP_IN_PROGRESS ) entry.add_to_hass(hass) + hass.config.components.add("comp") with pytest.raises( config_entries.OperationNotAllowed, @@ -4463,24 +4553,86 @@ def test_raise_trying_to_add_same_config_entry_twice( assert f"An entry with the id {entry.entry_id} already exists" in caplog.text +@pytest.mark.parametrize( + ( + "title", + "unique_id", + "data_vendor", + "options_vendor", + "kwargs", + "calls_entry_load_unload", + ), + [ + ( + ("Test", "Updated title"), + ("1234", "5678"), + ("data", "data2"), + ("options", "options2"), + {}, + (2, 1), + ), + ( + ("Test", "Test"), + ("1234", "1234"), + ("data", "data"), + ("options", "options"), + {}, + (2, 1), + ), + ( + ("Test", "Updated title"), + ("1234", "5678"), + ("data", "data2"), + ("options", "options2"), + {"reload_even_if_entry_is_unchanged": True}, + (2, 1), + ), + ( + ("Test", "Test"), + ("1234", "1234"), + ("data", "data"), + ("options", "options"), + {"reload_even_if_entry_is_unchanged": False}, + (1, 0), + ), + ], + ids=[ + "changed_entry_default", + "unchanged_entry_default", + "changed_entry_explicit_reload", + "changed_entry_no_reload", + ], +) async def test_update_entry_and_reload( - hass: HomeAssistant, manager: config_entries.ConfigEntries + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + title: tuple[str, str], + unique_id: tuple[str, str], + data_vendor: tuple[str, str], + options_vendor: tuple[str, str], + kwargs: dict[str, Any], + calls_entry_load_unload: tuple[int, int], ) -> None: """Test updating an entry and reloading.""" entry = MockConfigEntry( domain="comp", - unique_id="1234", - title="Test", - data={"vendor": "data"}, - options={"vendor": "options"}, + unique_id=unique_id[0], + title=title[0], + data={"vendor": data_vendor[0]}, + options={"vendor": options_vendor[0]}, ) entry.add_to_hass(hass) - mock_integration( - hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)) + comp = MockModule( + "comp", + async_setup_entry=AsyncMock(return_value=True), + async_unload_entry=AsyncMock(return_value=True), ) + mock_integration(hass, comp) mock_platform(hass, "comp.config_flow", None) + await hass.config_entries.async_setup(entry.entry_id) + class MockFlowHandler(config_entries.ConfigFlow): """Define a mock flow handler.""" @@ -4490,23 +4642,27 @@ async def test_update_entry_and_reload( """Mock Reauth.""" return self.async_update_reload_and_abort( entry=entry, - unique_id="5678", - title="Updated Title", - data={"vendor": "data2"}, - options={"vendor": "options2"}, + unique_id=unique_id[1], + title=title[1], + data={"vendor": data_vendor[1]}, + options={"vendor": options_vendor[1]}, + **kwargs, ) with patch.dict(config_entries.HANDLERS, {"comp": MockFlowHandler}): task = await manager.flow.async_init("comp", context={"source": "reauth"}) await hass.async_block_till_done() - assert entry.title == "Updated Title" - assert entry.unique_id == "5678" - assert entry.data == {"vendor": "data2"} - assert entry.options == {"vendor": "options2"} + assert entry.title == title[1] + assert entry.unique_id == unique_id[1] + assert entry.data == {"vendor": data_vendor[1]} + assert entry.options == {"vendor": options_vendor[1]} assert entry.state == config_entries.ConfigEntryState.LOADED assert task["type"] == FlowResultType.ABORT assert task["reason"] == "reauth_successful" + # Assert entry was reloaded + assert len(comp.async_setup_entry.mock_calls) == calls_entry_load_unload[0] + assert len(comp.async_unload_entry.mock_calls) == calls_entry_load_unload[1] @pytest.mark.parametrize("unique_id", [["blah", "bleh"], {"key": "value"}]) @@ -4909,3 +5065,48 @@ async def test_updating_non_added_entry_raises(hass: HomeAssistant) -> None: with pytest.raises(config_entries.UnknownEntry, match=entry.entry_id): hass.config_entries.async_update_entry(entry, unique_id="new_id") + + +async def test_reload_during_setup(hass: HomeAssistant) -> None: + """Test reload during setup waits.""" + entry = MockConfigEntry(domain="comp", data={"value": "initial"}) + entry.add_to_hass(hass) + + setup_start_future = hass.loop.create_future() + setup_finish_future = hass.loop.create_future() + in_setup = False + setup_calls = 0 + + async def mock_async_setup_entry(hass, entry): + """Mock setting up an entry.""" + nonlocal in_setup + nonlocal setup_calls + setup_calls += 1 + assert not in_setup + in_setup = True + setup_start_future.set_result(None) + await setup_finish_future + in_setup = False + return True + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_async_setup_entry, + async_unload_entry=AsyncMock(return_value=True), + ), + ) + mock_platform(hass, "comp.config_flow", None) + + setup_task = hass.async_create_task(async_setup_component(hass, "comp", {})) + + await setup_start_future # ensure we are in the setup + reload_task = hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) + ) + await asyncio.sleep(0) + setup_finish_future.set_result(None) + await setup_task + await reload_task + assert setup_calls == 2 diff --git a/tests/test_core.py b/tests/test_core.py index 905d8efe6de..66b5be718b1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -42,6 +42,7 @@ from homeassistant.core import ( CoreState, HassJob, HomeAssistant, + ReleaseChannel, ServiceCall, ServiceResponse, State, @@ -55,6 +56,7 @@ from homeassistant.exceptions import ( InvalidStateError, MaxLengthExceeded, ServiceNotFound, + ServiceValidationError, ) from homeassistant.helpers.json import json_dumps from homeassistant.setup import async_setup_component @@ -93,7 +95,7 @@ async def test_async_add_hass_job_schedule_callback() -> None: hass = MagicMock() job = MagicMock() - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(ha.callback(job))) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(ha.callback(job))) assert len(hass.loop.call_soon.mock_calls) == 1 assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -107,7 +109,7 @@ async def test_async_add_hass_job_eager_start_coro_suspends( async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_add_hass_job( + task = hass._async_add_hass_job( ha.HassJob(ha.callback(job_that_suspends)), eager_start=True ) assert not task.done() @@ -137,7 +139,7 @@ async def test_async_add_hass_job_background(hass: HomeAssistant) -> None: async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_add_hass_job( + task = hass._async_add_hass_job( ha.HassJob(ha.callback(job_that_suspends)), background=True ) assert not task.done() @@ -167,7 +169,7 @@ async def test_async_add_hass_job_eager_background(hass: HomeAssistant) -> None: async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_add_hass_job( + task = hass._async_add_hass_job( ha.HassJob(ha.callback(job_that_suspends)), background=True ) assert not task.done() @@ -232,7 +234,7 @@ async def test_async_add_hass_job_coro_named(hass: HomeAssistant) -> None: 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) + task = ha.HomeAssistant._async_add_hass_job(hass, job) assert "named coro" in str(task) @@ -245,7 +247,7 @@ async def test_async_add_hass_job_eager_start(hass: HomeAssistant) -> None: 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) + task = ha.HomeAssistant._async_add_hass_job(hass, job, eager_start=True) assert "named coro" in str(task) @@ -255,7 +257,7 @@ async def test_async_add_hass_job_schedule_partial_callback() -> None: job = MagicMock() partial = functools.partial(ha.callback(job)) - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(partial)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(partial)) assert len(hass.loop.call_soon.mock_calls) == 1 assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -268,7 +270,7 @@ async def test_async_add_hass_job_schedule_coroutinefunction() -> None: async def job(): pass - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(job)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(job)) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -285,7 +287,7 @@ async def test_async_add_hass_job_schedule_corofunction_eager_start() -> None: "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) + 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 @@ -301,7 +303,7 @@ async def test_async_add_hass_job_schedule_partial_coroutinefunction() -> None: partial = functools.partial(job) - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(partial)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(partial)) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -314,7 +316,7 @@ async def test_async_add_job_add_hass_threaded_job_to_pool() -> None: def job(): pass - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(job)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(job)) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.loop.run_in_executor.mock_calls) == 2 @@ -327,7 +329,7 @@ async def test_async_create_task_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job()) + ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=False) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -340,7 +342,7 @@ async def test_async_create_task_eager_start_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job(), eager_start=True) + ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True) # Should create the task directly since 3.12 supports eager_start assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -353,7 +355,9 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None: async def job(): pass - task = ha.HomeAssistant.async_create_task(hass, job(), "named task") + task = ha.HomeAssistant.async_create_task_internal( + hass, job(), "named task", eager_start=False + ) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -381,7 +385,7 @@ async def test_async_run_eager_hass_job_calls_coro_function() -> None: pass ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job)) - assert len(hass.async_add_hass_job.mock_calls) == 1 + assert len(hass._async_add_hass_job.mock_calls) == 1 async def test_async_run_hass_job_calls_callback() -> None: @@ -407,7 +411,7 @@ async def test_async_run_hass_job_delegates_non_async() -> None: ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job)) assert len(calls) == 0 - assert len(hass.async_add_hass_job.mock_calls) == 1 + assert len(hass._async_add_hass_job.mock_calls) == 1 async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: @@ -792,7 +796,7 @@ async def test_async_create_task_pending_tasks_coro(hass: HomeAssistant) -> None call_count.append("call") for _ in range(2): - hass.async_create_task(test_coro()) + hass.async_create_task(test_coro(), eager_start=False) assert len(hass._tasks) == 2 await hass.async_block_till_done() @@ -1144,11 +1148,11 @@ async def test_eventbus_filtered_listener(hass: HomeAssistant) -> None: calls.append(event) @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return not event_data["filtered"] - unsub = hass.bus.async_listen("test", listener, event_filter=filter) + unsub = hass.bus.async_listen("test", listener, event_filter=mock_filter) hass.bus.async_fire("test", {"filtered": True}) await hass.async_block_till_done() @@ -1172,7 +1176,7 @@ async def test_eventbus_run_immediately_callback(hass: HomeAssistant) -> None: """Mock listener.""" calls.append(event) - unsub = hass.bus.async_listen("test", listener, run_immediately=True) + unsub = hass.bus.async_listen("test", listener) hass.bus.async_fire("test", {"event": True}) # No async_block_till_done here @@ -1189,7 +1193,7 @@ async def test_eventbus_run_immediately_coro(hass: HomeAssistant) -> None: """Mock listener.""" calls.append(event) - unsub = hass.bus.async_listen("test", listener, run_immediately=True) + unsub = hass.bus.async_listen("test", listener) hass.bus.async_fire("test", {"event": True}) # No async_block_till_done here @@ -1206,7 +1210,7 @@ async def test_eventbus_listen_once_run_immediately_coro(hass: HomeAssistant) -> """Mock listener.""" calls.append(event) - hass.bus.async_listen_once("test", listener, run_immediately=True) + hass.bus.async_listen_once("test", listener) hass.bus.async_fire("test", {"event": True}) # No async_block_till_done here @@ -1789,8 +1793,9 @@ async def test_services_call_return_response_requires_blocking( hass: HomeAssistant, ) -> None: """Test that non-blocking service calls cannot ask for response data.""" + await async_setup_component(hass, "homeassistant", {}) async_mock_service(hass, "test_domain", "test_service") - with pytest.raises(ValueError, match="when blocking=False"): + with pytest.raises(ServiceValidationError, match="blocking=False") as exc: await hass.services.async_call( "test_domain", "test_service", @@ -1798,6 +1803,10 @@ async def test_services_call_return_response_requires_blocking( blocking=False, return_response=True, ) + assert str(exc.value) == ( + "A non blocking service call with argument blocking=False " + "can't be used together with argument return_response=True" + ) @pytest.mark.parametrize( @@ -1814,6 +1823,7 @@ async def test_serviceregistry_return_response_invalid( hass: HomeAssistant, response_data: Any, expected_error: str ) -> None: """Test service call response data must be json serializable objects.""" + await async_setup_component(hass, "homeassistant", {}) def service_handler(call: ServiceCall) -> ServiceResponse: """Service handler coroutine.""" @@ -1840,8 +1850,8 @@ async def test_serviceregistry_return_response_invalid( @pytest.mark.parametrize( ("supports_response", "return_response", "expected_error"), [ - (SupportsResponse.NONE, True, "not support responses"), - (SupportsResponse.ONLY, False, "caller did not ask for responses"), + (SupportsResponse.NONE, True, "does not return responses"), + (SupportsResponse.ONLY, False, "call requires responses"), ], ) async def test_serviceregistry_return_response_arguments( @@ -1851,6 +1861,7 @@ async def test_serviceregistry_return_response_arguments( expected_error: str, ) -> None: """Test service call response data invalid arguments.""" + await async_setup_component(hass, "homeassistant", {}) hass.services.async_register( "test_domain", @@ -1859,7 +1870,7 @@ async def test_serviceregistry_return_response_arguments( supports_response=supports_response, ) - with pytest.raises(ValueError, match=expected_error): + with pytest.raises(ServiceValidationError, match=expected_error): await hass.services.async_call( "test_domain", "test_service", @@ -1979,6 +1990,7 @@ async def test_config_as_dict() -> None: "country": None, "language": "en", "safe_mode": False, + "debug": False, } assert expected == config.as_dict() @@ -2374,11 +2386,11 @@ async def test_log_blocking_events( async def _wait_a_bit_2(): await asyncio.sleep(0.1) - hass.async_create_task(_wait_a_bit_1()) + hass.async_create_task(_wait_a_bit_1(), eager_start=False) await hass.async_block_till_done() with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001): - hass.async_create_task(_wait_a_bit_2()) + hass.async_create_task(_wait_a_bit_2(), eager_start=False) await hass.async_block_till_done() assert "_wait_a_bit_2" in caplog.text @@ -2398,14 +2410,14 @@ async def test_chained_logging_hits_log_timeout( created += 1 if created > 1000: return - hass.async_create_task(_task_chain_2()) + hass.async_create_task(_task_chain_2(), eager_start=False) async def _task_chain_2(): nonlocal created created += 1 if created > 1000: return - hass.async_create_task(_task_chain_1()) + hass.async_create_task(_task_chain_1(), eager_start=False) with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0): hass.async_create_task(_task_chain_1()) @@ -2427,16 +2439,16 @@ async def test_chained_logging_misses_log_timeout( created += 1 if created > 10: return - hass.async_create_task(_task_chain_2()) + hass.async_create_task(_task_chain_2(), eager_start=False) async def _task_chain_2(): nonlocal created created += 1 if created > 10: return - hass.async_create_task(_task_chain_1()) + hass.async_create_task(_task_chain_1(), eager_start=False) - hass.async_create_task(_task_chain_1()) + hass.async_create_task(_task_chain_1(), eager_start=False) await hass.async_block_till_done() assert "_task_chain_" not in caplog.text @@ -3050,13 +3062,15 @@ async def test_validate_state(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("version", "release_channel"), [ - ("0.115.0.dev20200815", "nightly"), - ("0.115.0", "stable"), - ("0.115.0b4", "beta"), - ("0.115.0dev0", "dev"), + ("0.115.0.dev20200815", ReleaseChannel.NIGHTLY), + ("0.115.0", ReleaseChannel.STABLE), + ("0.115.0b4", ReleaseChannel.BETA), + ("0.115.0dev0", ReleaseChannel.DEV), ], ) -async def test_get_release_channel(version: str, release_channel: str) -> None: +async def test_get_release_channel( + version: str, release_channel: ReleaseChannel +) -> None: """Test if release channel detection works from Home Assistant version number.""" with patch("homeassistant.core.__version__", f"{version}"): assert get_release_channel() == release_channel @@ -3234,6 +3248,23 @@ async def test_async_add_job_deprecated( ) in caplog.text +async def test_async_add_hass_job_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_add_hass_job warns about its deprecation.""" + + async def _test(): + pass + + hass.async_add_hass_job(HassJob(_test)) + assert ( + "Detected code that calls `async_add_hass_job`, which is deprecated " + "and will be removed in Home Assistant 2025.5; Please review " + "https://developers.home-assistant.io/blog/2024/04/07/deprecate_add_hass_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 = [] @@ -3244,11 +3275,11 @@ async def test_eventbus_lazy_object_creation(hass: HomeAssistant) -> None: calls.append(event) @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return not event_data["filtered"] - unsub = hass.bus.async_listen("test_1", listener, event_filter=filter) + unsub = hass.bus.async_listen("test_1", listener, event_filter=mock_filter) # Test lazy creation of Event objects with patch("homeassistant.core.Event") as mock_event: @@ -3313,7 +3344,7 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: """Test report state event.""" @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return True @@ -3324,9 +3355,7 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: 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.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=mock_filter) hass.states.async_set("light.bowl", "on") await hass.async_block_till_done() @@ -3357,19 +3386,109 @@ async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: """Mock listener.""" @ha.callback - def filter(event_data): + def mock_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) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener) # Both filter and run_immediately - hass.bus.async_listen( - EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=True - ) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=mock_filter) + + +@pytest.mark.parametrize( + "run_immediately", + [True, False], +) +@pytest.mark.parametrize( + "method", + ["async_listen", "async_listen_once"], +) +async def test_async_listen_with_run_immediately_deprecated( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + run_immediately: bool, + method: str, +) -> None: + """Test async_add_job warns about its deprecation.""" + + async def _test(event: ha.Event): + pass + + func = getattr(hass.bus, method) + func(EVENT_HOMEASSISTANT_START, _test, run_immediately=run_immediately) + assert ( + f"Detected code that calls `{method}` with run_immediately, which is " + "deprecated and will be removed in Home Assistant 2025.5." + ) in caplog.text + + +async def test_top_level_components(hass: HomeAssistant) -> None: + """Test top level components are updated when components change.""" + hass.config.components.add("homeassistant") + assert hass.config.components == {"homeassistant"} + assert hass.config.top_level_components == {"homeassistant"} + hass.config.components.add("homeassistant.scene") + assert hass.config.components == {"homeassistant", "homeassistant.scene"} + assert hass.config.top_level_components == {"homeassistant"} + hass.config.components.remove("homeassistant") + assert hass.config.components == {"homeassistant.scene"} + assert hass.config.top_level_components == set() + with pytest.raises(ValueError): + hass.config.components.remove("homeassistant.scene") + with pytest.raises(NotImplementedError): + hass.config.components.discard("homeassistant") + + +async def test_debug_mode_defaults_to_off(hass: HomeAssistant) -> None: + """Test debug mode defaults to off.""" + assert not hass.config.debug + + +async def test_async_fire_thread_safety(hass: HomeAssistant) -> None: + """Test async_fire thread safety.""" + events = async_capture_events(hass, "test_event") + hass.bus.async_fire("test_event") + with pytest.raises( + RuntimeError, match="Detected code that calls async_fire from a thread." + ): + await hass.async_add_executor_job(hass.bus.async_fire, "test_event") + + assert len(events) == 1 + + +async def test_async_register_thread_safety(hass: HomeAssistant) -> None: + """Test async_register thread safety.""" + with pytest.raises( + RuntimeError, match="Detected code that calls async_register from a thread." + ): + await hass.async_add_executor_job( + hass.services.async_register, + "test_domain", + "test_service", + lambda call: None, + ) + + +async def test_async_remove_thread_safety(hass: HomeAssistant) -> None: + """Test async_remove thread safety.""" + with pytest.raises( + RuntimeError, match="Detected code that calls async_remove from a thread." + ): + await hass.async_add_executor_job( + hass.services.async_remove, "test_domain", "test_service" + ) + + +async def test_async_create_task_thread_safety(hass: HomeAssistant) -> None: + """Test async_create_task thread safety.""" + + async def _any_coro(): + pass + + with pytest.raises( + RuntimeError, match="Detected code that calls async_create_task from a thread." + ): + await hass.async_add_executor_job(hass.async_create_task, _any_coro) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 5c3ad2a3b39..312e2be7602 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -189,7 +189,7 @@ async def test_abort_calls_async_remove_with_exception( with caplog.at_level(logging.ERROR): await manager.async_init("test") - assert "Error removing test flow: error" in caplog.text + assert "Error removing test flow" in caplog.text TestFlow.async_remove.assert_called_once() @@ -269,8 +269,7 @@ async def test_finish_callback_change_result_type(hass: HomeAssistant) -> None: return flow.async_show_form( step_id="init", data_schema=vol.Schema({"count": int}) ) - else: - result["result"] = result["data"]["count"] + result["result"] = result["data"]["count"] return result manager = FlowManager(hass) @@ -400,7 +399,6 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: hass.bus.async_listen( data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED, capture_events, - run_immediately=True, ) result = await manager.async_init("test") @@ -462,6 +460,7 @@ async def test_show_progress_error(hass: HomeAssistant, manager) -> None: async def async_step_init(self, user_input=None): async def long_running_task() -> None: + await asyncio.sleep(0) raise TypeError if not self.progress_task: @@ -480,7 +479,6 @@ async def test_show_progress_error(hass: HomeAssistant, manager) -> None: hass.bus.async_listen( data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED, capture_events, - run_immediately=True, ) result = await manager.async_init("test") @@ -521,7 +519,7 @@ async def test_show_progress_hidden_from_frontend(hass: HomeAssistant, manager) nonlocal progress_task async def long_running_job() -> None: - return + await asyncio.sleep(0) if not progress_task: progress_task = hass.async_create_task(long_running_job()) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5e113d3ba10..9d556b55b15 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -102,7 +102,7 @@ def test_template_message(arg: str | Exception, expected: str) -> None: ) async def test_home_assistant_error( hass: HomeAssistant, - exception_args: tuple[Any,], + exception_args: tuple[Any, ...], exception_kwargs: dict[str, Any], args_base_class: tuple[Any], message: str, diff --git a/tests/test_loader.py b/tests/test_loader.py index 6685bb4f2ac..404858200bc 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1408,7 +1408,7 @@ async def test_async_get_component_concurrent_loads( modules_without_integration = { k: v for k, v in sys.modules.items() - if k != config_flow_module_name and k != integration.pkg_path + if k not in (config_flow_module_name, integration.pkg_path) } with ( patch.dict( @@ -1471,6 +1471,50 @@ async def test_async_get_component_deadlock_fallback( assert module is module_mock +async def test_async_get_component_deadlock_fallback_module_not_found( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Verify async_get_component fallback behavior. + + Ensure that fallback is not triggered on ModuleNotFoundError. + """ + executor_import_integration = _get_test_integration( + hass, "executor_import", True, import_executor=True + ) + assert executor_import_integration.import_executor is True + module_mock = MagicMock(__file__="__init__.py") + import_attempts = 0 + + def mock_import(module: str, *args: Any, **kwargs: Any) -> Any: + nonlocal import_attempts + if module == "homeassistant.components.executor_import": + import_attempts += 1 + + if import_attempts == 1: + raise ModuleNotFoundError( + "homeassistant.components.executor_import not found", + name="homeassistant.components.executor_import", + ) + + return module_mock + + 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( + ModuleNotFoundError, match="homeassistant.components.executor_import" + ), + ): + await executor_import_integration.async_get_component() + + # We should not have tried to fall back to the event loop import + assert "loaded_executor=False" not in caplog.text + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + assert import_attempts == 1 + + async def test_async_get_component_raises_after_import_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1551,6 +1595,52 @@ async def test_async_get_platform_deadlock_fallback( assert module is module_mock +async def test_async_get_platform_deadlock_fallback_module_not_found( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Verify async_get_platform fallback behavior. + + Ensure that fallback is not triggered on ModuleNotFoundError. + """ + executor_import_integration = _get_test_integration( + hass, "executor_import", True, import_executor=True + ) + assert executor_import_integration.import_executor is True + module_mock = MagicMock() + import_attempts = 0 + + def mock_import(module: str, *args: Any, **kwargs: Any) -> Any: + nonlocal import_attempts + if module == "homeassistant.components.executor_import.config_flow": + import_attempts += 1 + + if import_attempts == 1: + raise ModuleNotFoundError( + "Not found homeassistant.components.executor_import.config_flow", + name="homeassistant.components.executor_import.config_flow", + ) + + return module_mock + + 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( + ModuleNotFoundError, + match="homeassistant.components.executor_import.config_flow", + ), + ): + await executor_import_integration.async_get_platform("config_flow") + + # We should not have tried to fall back to the event loop import + assert "executor=['config_flow']" in caplog.text + assert "loop=['config_flow']" not in caplog.text + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + assert import_attempts == 1 + + async def test_async_get_platform_raises_after_import_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1796,7 +1886,7 @@ async def test_async_get_platforms_concurrent_loads( modules_without_button = { k: v for k, v in sys.modules.items() - if k != button_module_name and k != integration.pkg_path + if k not in (button_module_name, integration.pkg_path) } with ( patch.dict( @@ -1838,3 +1928,34 @@ async def test_has_services(hass: HomeAssistant, enable_custom_integrations) -> assert integration.has_services is False integration = await loader.async_get_integration(hass, "test_with_services") assert integration.has_services is True + + +async def test_hass_helpers_use_reported( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock +) -> None: + """Test that use of hass.components is reported.""" + integration_frame = frame.IntegrationFrame( + custom_integration=True, + _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.object(frame, "_REPORTED_INTEGRATIONS", new=set()), + patch( + "homeassistant.helpers.frame.get_integration_frame", + return_value=integration_frame, + ), + patch( + "homeassistant.helpers.aiohttp_client.async_get_clientsession", + return_value=None, + ), + ): + hass.helpers.aiohttp_client.async_get_clientsession() + + assert ( + "Detected that custom integration 'test_integration_frame' " + "accesses hass.helpers.aiohttp_client. This is deprecated" + ) in caplog.text diff --git a/tests/test_requirements.py b/tests/test_requirements.py index ed04ef8649b..73f3f54c3c4 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -591,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) == 2 # mqtt also depends on http + assert len(mock_process.mock_calls) == 1 assert mock_process.mock_calls[0][1][1] == mqtt.requirements @@ -608,13 +608,12 @@ async def test_discovery_requirements_ssdp(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 4 + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == ssdp.requirements assert { mock_process.mock_calls[1][1][0], mock_process.mock_calls[2][1][0], - mock_process.mock_calls[3][1][0], - } == {"http", "network", "recorder"} + } == {"network", "recorder"} @pytest.mark.parametrize( @@ -638,7 +637,7 @@ async def test_discovery_requirements_zeroconf( ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 4 # zeroconf also depends on http + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == zeroconf.requirements diff --git a/tests/test_setup.py b/tests/test_setup.py index e3d9a322862..65472643adb 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -346,8 +346,9 @@ async def test_component_base_exception_setup(hass: HomeAssistant) -> None: mock_integration(hass, MockModule("comp", setup=exception_setup)) - with pytest.raises(BaseException): + with pytest.raises(BaseException) as exc_info: await setup.async_setup_component(hass, "comp", {}) + assert str(exc_info.value) == "fail!" assert "comp" not in hass.config.components diff --git a/tests/testing_config/custom_components/ruff.toml b/tests/testing_config/custom_components/ruff.toml new file mode 100644 index 00000000000..00a6d7ef849 --- /dev/null +++ b/tests/testing_config/custom_components/ruff.toml @@ -0,0 +1,6 @@ +extend = "../../ruff.toml" + +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] diff --git a/tests/testing_config/custom_components/test/date.py b/tests/testing_config/custom_components/test/date.py deleted file mode 100644 index 0a51bea029d..00000000000 --- a/tests/testing_config/custom_components/test/date.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Provide a mock date platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from datetime import date - -from homeassistant.components.date import DateEntity - -from tests.common import MockEntity - -UNIQUE_DATE = "unique_date" - -ENTITIES = [] - - -class MockDateEntity(MockEntity, DateEntity): - """Mock date class.""" - - @property - def native_value(self): - """Return the native value of this date.""" - return self._handle("native_value") - - def set_value(self, value: date) -> None: - """Change the date.""" - self._values["native_value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockDateEntity( - name="test", - unique_id=UNIQUE_DATE, - native_value=date(2020, 1, 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/datetime.py b/tests/testing_config/custom_components/test/datetime.py deleted file mode 100644 index fa9dfff8a60..00000000000 --- a/tests/testing_config/custom_components/test/datetime.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Provide a mock time platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from datetime import UTC, datetime - -from homeassistant.components.datetime import DateTimeEntity - -from tests.common import MockEntity - -UNIQUE_DATETIME = "unique_datetime" - -ENTITIES = [] - - -class MockDateTimeEntity(MockEntity, DateTimeEntity): - """Mock date/time class.""" - - @property - def native_value(self): - """Return the native value of this date/time.""" - return self._handle("native_value") - - def set_value(self, value: datetime) -> None: - """Change the time.""" - self._values["native_value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockDateTimeEntity( - name="test", - unique_id=UNIQUE_DATETIME, - native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=UTC), - ), - ] - ) - - -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/device_tracker.py b/tests/testing_config/custom_components/test/device_tracker.py deleted file mode 100644 index 11eb366f2fc..00000000000 --- a/tests/testing_config/custom_components/test/device_tracker.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Provide a mock device scanner.""" - -from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import SourceType - - -async def async_get_scanner(hass, config): - """Return a mock scanner.""" - return SCANNER - - -class MockScannerEntity(ScannerEntity): - """Test implementation of a ScannerEntity.""" - - def __init__(self): - """Init.""" - self.connected = False - self._hostname = "test.hostname.org" - self._ip_address = "0.0.0.0" - self._mac_address = "ad:de:ef:be:ed:fe" - - @property - def source_type(self): - """Return the source type, eg gps or router, of the device.""" - return SourceType.ROUTER - - @property - def battery_level(self): - """Return the battery level of the device. - - Percentage from 0-100. - """ - return 100 - - @property - def ip_address(self) -> str: - """Return the primary ip address of the device.""" - return self._ip_address - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self._mac_address - - @property - def hostname(self) -> str: - """Return hostname of the device.""" - return self._hostname - - @property - def is_connected(self): - """Return true if the device is connected to the network.""" - return self.connected - - def set_connected(self): - """Set connected to True.""" - self.connected = True - self.async_schedule_update_ha_state() - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the config entry.""" - entity = MockScannerEntity() - async_add_entities([entity]) - - -class MockScanner(DeviceScanner): - """Mock device scanner.""" - - def __init__(self): - """Initialize the MockScanner.""" - self.devices_home = [] - - def come_home(self, device): - """Make a device come home.""" - self.devices_home.append(device) - - def leave_home(self, device): - """Make a device leave the house.""" - self.devices_home.remove(device) - - def reset(self): - """Reset which devices are home.""" - self.devices_home = [] - - def scan_devices(self): - """Return a list of fake devices.""" - return list(self.devices_home) - - def get_device_name(self, device): - """Return a name for a mock device. - - Return None for dev1 for testing. - """ - return None if device == "DEV1" else device.lower() - - -SCANNER = MockScanner() diff --git a/tests/testing_config/custom_components/test/fan.py b/tests/testing_config/custom_components/test/fan.py deleted file mode 100644 index cc38972bc71..00000000000 --- a/tests/testing_config/custom_components/test/fan.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Provide a mock fan platform. - -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 -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from tests.common import MockEntity - -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - "support_preset_mode": MockFan( - name="Support fan with preset_mode support", - supported_features=FanEntityFeature.PRESET_MODE, - unique_id="unique_support_preset_mode", - preset_modes=["auto", "eco"], - ) - } - ) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities_callback: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - - -class MockFan(MockEntity, FanEntity): - """Mock Fan class.""" - - @property - def preset_mode(self) -> str | None: - """Return preset mode.""" - return self._handle("preset_mode") - - @property - def preset_modes(self) -> list[str] | None: - """Return preset mode.""" - return self._handle("preset_modes") - - @property - def supported_features(self): - """Return the class of this fan.""" - return self._handle("supported_features") - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set preset mode.""" - self._attr_preset_mode = preset_mode - await self.async_update_ha_state() diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index eed98a8210a..4cd49fec606 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -13,7 +13,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = ( [] diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index ba5a91e2d24..e97d3f8de22 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -12,7 +12,7 @@ ENTITIES = {} def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = ( {} diff --git a/tests/testing_config/custom_components/test/remote.py b/tests/testing_config/custom_components/test/remote.py index 541215f1c47..3226c93310c 100644 --- a/tests/testing_config/custom_components/test/remote.py +++ b/tests/testing_config/custom_components/test/remote.py @@ -13,7 +13,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = ( [] diff --git a/tests/testing_config/custom_components/test/select.py b/tests/testing_config/custom_components/test/select.py deleted file mode 100644 index fece370bdf1..00000000000 --- a/tests/testing_config/custom_components/test/select.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Provide a mock select platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from homeassistant.components.select import SelectEntity - -from tests.common import MockEntity - -UNIQUE_SELECT_1 = "unique_select_1" -UNIQUE_SELECT_2 = "unique_select_2" - -ENTITIES = [] - - -class MockSelectEntity(MockEntity, SelectEntity): - """Mock Select class.""" - - _attr_current_option = None - - @property - def current_option(self): - """Return the current option of this select.""" - return self._handle("current_option") - - @property - def options(self) -> list: - """Return the list of available options of this select.""" - return self._handle("options") - - def select_option(self, option: str) -> None: - """Change the selected option.""" - self._attr_current_option = option - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockSelectEntity( - name="select 1", - unique_id="unique_select_1", - options=["option 1", "option 2", "option 3"], - current_option="option 1", - ), - MockSelectEntity( - name="select 2", - unique_id="unique_select_2", - options=["option 1", "option 2", "option 3"], - ), - ] - ) - - -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/switch.py b/tests/testing_config/custom_components/test/switch.py index 5a2cd7bc17d..b06db33746f 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -1,32 +1,8 @@ -"""Provide a mock switch platform. - -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 - -ENTITIES = [] - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockToggleEntity("AC", STATE_ON), - MockToggleEntity("AC", STATE_OFF), - MockToggleEntity(None, STATE_OFF), - ] - ) +"""Stub switch platform for translation tests.""" async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) + """Stub setup for translation tests.""" + async_add_entities_callback([]) diff --git a/tests/testing_config/custom_components/test/text.py b/tests/testing_config/custom_components/test/text.py deleted file mode 100644 index d3b048747bf..00000000000 --- a/tests/testing_config/custom_components/test/text.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Provide a mock text platform. - -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 - -UNIQUE_TEXT = "unique_text" - -ENTITIES = [] - - -class MockTextEntity(MockEntity, TextEntity): - """Mock text class.""" - - @property - def native_max(self): - """Return the native native_max.""" - return self._handle("native_max") - - @property - def native_min(self): - """Return the native native_min.""" - return self._handle("native_min") - - @property - def mode(self): - """Return the mode.""" - return self._handle("mode") - - @property - def pattern(self): - """Return the pattern.""" - return self._handle("pattern") - - @property - def native_value(self): - """Return the native value of this text.""" - return self._handle("native_value") - - def set_native_value(self, value: str) -> None: - """Change the selected option.""" - self._values["native_value"] = value - - -class MockRestoreText(MockTextEntity, RestoreText): - """Mock RestoreText class.""" - - async def async_added_to_hass(self) -> None: - """Restore native_*.""" - await super().async_added_to_hass() - if (last_text_data := await self.async_get_last_text_data()) is None: - return - self._values["native_max"] = last_text_data.native_max - self._values["native_min"] = last_text_data.native_min - self._values["native_value"] = last_text_data.native_value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockTextEntity( - name="test", - native_min=1, - native_max=5, - mode=TextMode.TEXT, - pattern=r"[A-Za-z0-9]", - unique_id=UNIQUE_TEXT, - native_value="Hello", - ), - ] - ) - - -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/time.py b/tests/testing_config/custom_components/test/time.py deleted file mode 100644 index 998406d7830..00000000000 --- a/tests/testing_config/custom_components/test/time.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Provide a mock time platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from datetime import time - -from homeassistant.components.time import TimeEntity - -from tests.common import MockEntity - -UNIQUE_TIME = "unique_time" - -ENTITIES = [] - - -class MockTimeEntity(MockEntity, TimeEntity): - """Mock time class.""" - - @property - def native_value(self): - """Return the native value of this time.""" - return self._handle("native_value") - - def set_value(self, value: time) -> None: - """Change the time.""" - self._values["native_value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockTimeEntity( - name="test", - unique_id=UNIQUE_TIME, - native_value=time(1, 2, 3), - ), - ] - ) - - -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/translations/_broken.en.json b/tests/testing_config/custom_components/test/translations/_broken.json similarity index 100% rename from tests/testing_config/custom_components/test/translations/_broken.en.json rename to tests/testing_config/custom_components/test/translations/_broken.json diff --git a/tests/testing_config/custom_components/test/translations/en.json b/tests/testing_config/custom_components/test/translations/en.json index 56404508c4c..7ed32c224a7 100644 --- a/tests/testing_config/custom_components/test/translations/en.json +++ b/tests/testing_config/custom_components/test/translations/en.json @@ -7,5 +7,6 @@ "other4": { "name": "Other 4" }, "outlet": { "name": "Outlet {placeholder}" } } - } + }, + "something": "else" } diff --git a/tests/testing_config/custom_components/test/translations/switch.de.json b/tests/testing_config/custom_components/test/translations/switch.de.json deleted file mode 100644 index fad78b12d63..00000000000 --- a/tests/testing_config/custom_components/test/translations/switch.de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "string1": "German Value 1", - "string2": "German Value 2" - } -} diff --git a/tests/testing_config/custom_components/test/translations/switch.en.json b/tests/testing_config/custom_components/test/translations/switch.en.json deleted file mode 100644 index 1cc764adb21..00000000000 --- a/tests/testing_config/custom_components/test/translations/switch.en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "string1": "Value 1", - "string2": "Value 2" - }, - "something": "else" -} diff --git a/tests/testing_config/custom_components/test/translations/switch.es.json b/tests/testing_config/custom_components/test/translations/switch.es.json deleted file mode 100644 index b3590a6d321..00000000000 --- a/tests/testing_config/custom_components/test/translations/switch.es.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "state": { - "string1": "Spanish Value 1" - } -} diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 0e99ef48680..b051531b9e8 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -33,7 +33,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = [] if empty else [MockWeather()] diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 50eecec72f6..ac927b1375a 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -6,7 +6,6 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from homeassistant import block_async_io from homeassistant.core import HomeAssistant from homeassistant.util import async_ as hasync @@ -38,172 +37,6 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _) -> None: assert len(loop.call_soon_threadsafe.mock_calls) == 2 -def banned_function(): - """Mock banned function.""" - - -async def test_check_loop_async() -> None: - """Test check_loop detects when called from event loop without integration context.""" - with pytest.raises(RuntimeError): - hasync.check_loop(banned_function) - - -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.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()", - ), - ] - ), - ), - ): - hasync.check_loop(banned_function) - assert ( - "Detected blocking call to banned_function inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on, " - "please create a bug report at https://github.com/home-assistant/core/issues?" - "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text - ) - - -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.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()", - ), - ] - ), - ), - ): - hasync.check_loop(banned_function, strict=False) - assert ( - "Detected blocking call to banned_function inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on, " - "please create a bug report at https://github.com/home-assistant/core/issues?" - "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text - ) - - -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.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()", - ), - ] - ), - ), - ): - hasync.check_loop(banned_function) - assert ( - "Detected blocking call to banned_function inside the event loop by custom " - "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" - ", please create a bug report at https://github.com/home-assistant/core/issues?" - "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" - ) in caplog.text - - -def test_check_loop_sync(caplog: pytest.LogCaptureFixture) -> None: - """Test check_loop does nothing when called from thread.""" - hasync.check_loop(banned_function) - assert "Detected blocking call inside the event loop" not in caplog.text - - -def test_protect_loop_sync() -> None: - """Test protect_loop calls check_loop.""" - func = Mock() - with patch("homeassistant.util.async_.check_loop") as mock_check_loop: - hasync.protect_loop(func)(1, test=2) - mock_check_loop.assert_called_once_with(func, strict=True) - func.assert_called_once_with(1, test=2) - - -async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> None: - """Test time.sleep injected by the debugger is not reported.""" - block_async_io.enable() - - with patch( - "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 - - async def test_gather_with_limited_concurrency() -> None: """Test gather_with_limited_concurrency limits the number of running tasks.""" @@ -243,7 +76,8 @@ async def test_run_callback_threadsafe(hass: HomeAssistant) -> None: nonlocal it_ran it_ran = True - assert hasync.run_callback_threadsafe(hass.loop, callback) + with patch.dict(hass.loop.__dict__, {"_thread_ident": -1}): + assert hasync.run_callback_threadsafe(hass.loop, callback) assert it_ran is False # Verify that async_block_till_done will flush @@ -262,6 +96,7 @@ async def test_callback_is_always_scheduled(hass: HomeAssistant) -> None: hasync.shutdown_run_callback_threadsafe(hass.loop) with ( + patch.dict(hass.loop.__dict__, {"_thread_ident": -1}), patch.object(hass.loop, "call_soon_threadsafe") as mock_call_soon_threadsafe, pytest.raises(RuntimeError), ): @@ -292,3 +127,73 @@ async def test_create_eager_task_312(hass: HomeAssistant) -> None: assert events == ["eager", "normal"] await task1 await task2 + + +async def test_create_eager_task_from_thread(hass: HomeAssistant) -> None: + """Test we report trying to create an eager task from a thread.""" + + def create_task(): + hasync.create_eager_task(asyncio.sleep(0)) + + with pytest.raises( + RuntimeError, + match=( + "Detected code that attempted to create an asyncio task from a thread. Please report this issue." + ), + ): + await hass.async_add_executor_job(create_task) + + +async def test_create_eager_task_from_thread_in_integration( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we report trying to create an eager task from a thread.""" + + def create_task(): + hasync.create_eager_task(asyncio.sleep(0)) + + frames = 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()", + ), + ] + ) + with ( + pytest.raises(RuntimeError, match="no running event loop"), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + await hass.async_add_executor_job(create_task) + + assert ( + "Detected that integration 'hue' attempted to create an asyncio task " + "from a thread at homeassistant/components/hue/light.py, line 23: " + "self.light.is_on" + ) in caplog.text diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 5716e4e524c..215524c426b 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -88,12 +88,6 @@ def test_as_local_with_naive_object() -> None: ) < timedelta(seconds=1) -def test_as_local_with_local_object() -> None: - """Test local with local object.""" - now = dt_util.now() - assert now == now - - def test_as_local_with_utc_object() -> None: """Test local time with UTC object.""" dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) @@ -184,12 +178,18 @@ def test_get_age() -> None: """Test get_age.""" diff = dt_util.now() - timedelta(seconds=0) assert dt_util.get_age(diff) == "0 seconds" + assert dt_util.get_age(diff, precision=2) == "0 seconds" diff = dt_util.now() - timedelta(seconds=1) assert dt_util.get_age(diff) == "1 second" + assert dt_util.get_age(diff, precision=2) == "1 second" + + diff = dt_util.now() + timedelta(seconds=1) + pytest.raises(ValueError, dt_util.get_age, diff) diff = dt_util.now() - timedelta(seconds=30) assert dt_util.get_age(diff) == "30 seconds" + diff = dt_util.now() + timedelta(seconds=30) diff = dt_util.now() - timedelta(minutes=5) assert dt_util.get_age(diff) == "5 minutes" @@ -202,20 +202,81 @@ def test_get_age() -> None: diff = dt_util.now() - timedelta(minutes=320) assert dt_util.get_age(diff) == "5 hours" + assert dt_util.get_age(diff, precision=2) == "5 hours 20 minutes" + assert dt_util.get_age(diff, precision=3) == "5 hours 20 minutes" diff = dt_util.now() - timedelta(minutes=1.6 * 60 * 24) assert dt_util.get_age(diff) == "2 days" + assert dt_util.get_age(diff, precision=2) == "1 day 14 hours" + assert dt_util.get_age(diff, precision=3) == "1 day 14 hours 24 minutes" + diff = dt_util.now() + timedelta(minutes=1.6 * 60 * 24) + pytest.raises(ValueError, dt_util.get_age, diff) diff = dt_util.now() - timedelta(minutes=2 * 60 * 24) assert dt_util.get_age(diff) == "2 days" diff = dt_util.now() - timedelta(minutes=32 * 60 * 24) assert dt_util.get_age(diff) == "1 month" + assert dt_util.get_age(diff, precision=10) == "1 month 2 days" + + diff = dt_util.now() - timedelta(minutes=32 * 60 * 24 + 1) + assert dt_util.get_age(diff, precision=3) == "1 month 2 days 1 minute" diff = dt_util.now() - timedelta(minutes=365 * 60 * 24) assert dt_util.get_age(diff) == "1 year" +def test_time_remaining() -> None: + """Test get_age.""" + diff = dt_util.now() + timedelta(seconds=0) + assert dt_util.get_time_remaining(diff) == "0 seconds" + assert dt_util.get_time_remaining(diff) == "0 seconds" + assert dt_util.get_time_remaining(diff, precision=2) == "0 seconds" + + diff = dt_util.now() + timedelta(seconds=1) + assert dt_util.get_time_remaining(diff) == "1 second" + + diff = dt_util.now() - timedelta(seconds=1) + pytest.raises(ValueError, dt_util.get_time_remaining, diff) + + diff = dt_util.now() + timedelta(seconds=30) + assert dt_util.get_time_remaining(diff) == "30 seconds" + + diff = dt_util.now() + timedelta(minutes=5) + assert dt_util.get_time_remaining(diff) == "5 minutes" + + diff = dt_util.now() + timedelta(minutes=1) + assert dt_util.get_time_remaining(diff) == "1 minute" + + diff = dt_util.now() + timedelta(minutes=300) + assert dt_util.get_time_remaining(diff) == "5 hours" + + diff = dt_util.now() + timedelta(minutes=320) + assert dt_util.get_time_remaining(diff) == "5 hours" + assert dt_util.get_time_remaining(diff, precision=2) == "5 hours 20 minutes" + assert dt_util.get_time_remaining(diff, precision=3) == "5 hours 20 minutes" + + diff = dt_util.now() + timedelta(minutes=1.6 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "2 days" + assert dt_util.get_time_remaining(diff, precision=2) == "1 day 14 hours" + assert dt_util.get_time_remaining(diff, precision=3) == "1 day 14 hours 24 minutes" + diff = dt_util.now() - timedelta(minutes=1.6 * 60 * 24) + pytest.raises(ValueError, dt_util.get_time_remaining, diff) + + diff = dt_util.now() + timedelta(minutes=2 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "2 days" + + diff = dt_util.now() + timedelta(minutes=32 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "1 month" + assert dt_util.get_time_remaining(diff, precision=10) == "1 month 2 days" + + diff = dt_util.now() + timedelta(minutes=32 * 60 * 24 + 1) + assert dt_util.get_time_remaining(diff, precision=3) == "1 month 2 days 1 minute" + + diff = dt_util.now() + timedelta(minutes=365 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "1 year" + + def test_parse_time_expression() -> None: """Test parse_time_expression.""" assert list(range(60)) == dt_util.parse_time_expression("*", 0, 59) diff --git a/tests/util/test_event_type.py b/tests/util/test_event_type.py new file mode 100644 index 00000000000..3086c8ea075 --- /dev/null +++ b/tests/util/test_event_type.py @@ -0,0 +1,25 @@ +"""Test EventType implementation.""" + +from __future__ import annotations + +import orjson + +from homeassistant.util.event_type import EventType + + +def test_compatibility_with_str() -> None: + """Test EventType. At runtime it should be (almost) fully compatible with str.""" + + event = EventType("Hello World") + assert event == "Hello World" + assert len(event) == 11 + assert hash(event) == hash("Hello World") + d: dict[str | EventType, int] = {EventType("key"): 2} + assert d["key"] == 2 + + +def test_json_dump() -> None: + """Test EventType json dump with orjson.""" + + event = EventType("state_changed") + assert orjson.dumps({"event_type": event}) == b'{"event_type":"state_changed"}' diff --git a/tests/util/test_json.py b/tests/util/test_json.py index b4a52cb4b41..3eccb524538 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -163,7 +163,7 @@ async def test_loading_derived_class(): """Test loading data from classes derived from str.""" class MyStr(str): - pass + __slots__ = () class MyBytes(bytes): pass diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 53342e8d1bd..8e7106475a2 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -20,7 +20,7 @@ import homeassistant.util.logging as logging_util async def test_logging_with_queue_handler() -> None: """Test logging with HomeAssistantQueueHandler.""" - simple_queue = queue.SimpleQueue() # type: ignore + simple_queue = queue.SimpleQueue() handler = logging_util.HomeAssistantQueueHandler(simple_queue) log_record = logging.makeLogRecord({"msg": "Test Log Record"}) diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py new file mode 100644 index 00000000000..8b4465bef2b --- /dev/null +++ b/tests/util/test_loop.py @@ -0,0 +1,200 @@ +"""Tests for async util methods from Python source.""" + +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.util import loop as haloop + +from tests.common import extract_stack_to_frame + + +def banned_function(): + """Mock banned function.""" + + +async def test_check_loop_async() -> None: + """Test check_loop detects when called from event loop without integration context.""" + with pytest.raises(RuntimeError): + haloop.check_loop(banned_function) + + +async def test_check_loop_async_non_strict_core( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test non_strict_core check_loop detects from event loop without integration context.""" + haloop.check_loop(banned_function, strict_core=False) + assert "Detected blocking call to banned_function" in caplog.text + + +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.""" + frames = 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()", + ), + ] + ) + with ( + pytest.raises(RuntimeError), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + haloop.check_loop(banned_function) + assert ( + "Detected blocking call to banned_function inside the event loop by integration" + " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), please create " + "a bug report at https://github.com/home-assistant/core/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text + ) + + +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.""" + frames = 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()", + ), + ] + ) + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + haloop.check_loop(banned_function, strict=False) + assert ( + "Detected blocking call to banned_function inside the event loop by integration" + " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + "please create a bug report at https://github.com/home-assistant/core/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text + ) + + +async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None: + """Test check_loop detects when called from event loop with custom component context.""" + frames = 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()", + ), + ] + ) + with ( + pytest.raises(RuntimeError), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + haloop.check_loop(banned_function) + assert ( + "Detected blocking call to banned_function inside the event loop by custom " + "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" + " (offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + "please create a bug report at https://github.com/home-assistant/core/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" + ) in caplog.text + + +def test_check_loop_sync(caplog: pytest.LogCaptureFixture) -> None: + """Test check_loop does nothing when called from thread.""" + haloop.check_loop(banned_function) + assert "Detected blocking call inside the event loop" not in caplog.text + + +def test_protect_loop_sync() -> None: + """Test protect_loop calls check_loop.""" + func = Mock() + with patch("homeassistant.util.loop.check_loop") as mock_check_loop: + haloop.protect_loop(func)(1, test=2) + mock_check_loop.assert_called_once_with( + func, + strict=True, + args=(1,), + check_allowed=None, + kwargs={"test": 2}, + strict_core=True, + ) + func.assert_called_once_with(1, test=2) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 0e2e9278676..2ead327bf10 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock, call, patch import pytest -import homeassistant.util.package as package +from homeassistant.util import package RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") diff --git a/tests/util/test_percentage.py b/tests/util/test_percentage.py index 2fc054fb4f1..3af42310e94 100644 --- a/tests/util/test_percentage.py +++ b/tests/util/test_percentage.py @@ -104,77 +104,77 @@ async def test_percentage_to_ordered_list_item() -> None: async def test_ranged_value_to_percentage_large() -> None: """Test a large range of low and high values convert a single value to a percentage.""" - range = (1, 255) + value_range = (1, 255) - assert ranged_value_to_percentage(range, 255) == 100 - assert ranged_value_to_percentage(range, 127) == 49 - assert ranged_value_to_percentage(range, 10) == 3 - assert ranged_value_to_percentage(range, 1) == 0 + assert ranged_value_to_percentage(value_range, 255) == 100 + assert ranged_value_to_percentage(value_range, 127) == 49 + assert ranged_value_to_percentage(value_range, 10) == 3 + assert ranged_value_to_percentage(value_range, 1) == 0 async def test_percentage_to_ranged_value_large() -> None: """Test a large range of low and high values convert a percentage to a single value.""" - range = (1, 255) + value_range = (1, 255) - assert percentage_to_ranged_value(range, 100) == 255 - assert percentage_to_ranged_value(range, 50) == 127.5 - assert percentage_to_ranged_value(range, 4) == 10.2 + assert percentage_to_ranged_value(value_range, 100) == 255 + assert percentage_to_ranged_value(value_range, 50) == 127.5 + assert percentage_to_ranged_value(value_range, 4) == 10.2 - assert math.ceil(percentage_to_ranged_value(range, 100)) == 255 - assert math.ceil(percentage_to_ranged_value(range, 50)) == 128 - assert math.ceil(percentage_to_ranged_value(range, 4)) == 11 + assert math.ceil(percentage_to_ranged_value(value_range, 100)) == 255 + assert math.ceil(percentage_to_ranged_value(value_range, 50)) == 128 + assert math.ceil(percentage_to_ranged_value(value_range, 4)) == 11 async def test_ranged_value_to_percentage_small() -> None: """Test a small range of low and high values convert a single value to a percentage.""" - range = (1, 6) + value_range = (1, 6) - assert ranged_value_to_percentage(range, 1) == 16 - assert ranged_value_to_percentage(range, 2) == 33 - assert ranged_value_to_percentage(range, 3) == 50 - assert ranged_value_to_percentage(range, 4) == 66 - assert ranged_value_to_percentage(range, 5) == 83 - assert ranged_value_to_percentage(range, 6) == 100 + assert ranged_value_to_percentage(value_range, 1) == 16 + assert ranged_value_to_percentage(value_range, 2) == 33 + assert ranged_value_to_percentage(value_range, 3) == 50 + assert ranged_value_to_percentage(value_range, 4) == 66 + assert ranged_value_to_percentage(value_range, 5) == 83 + assert ranged_value_to_percentage(value_range, 6) == 100 async def test_percentage_to_ranged_value_small() -> None: """Test a small range of low and high values convert a percentage to a single value.""" - range = (1, 6) + value_range = (1, 6) - assert math.ceil(percentage_to_ranged_value(range, 16)) == 1 - assert math.ceil(percentage_to_ranged_value(range, 33)) == 2 - assert math.ceil(percentage_to_ranged_value(range, 50)) == 3 - assert math.ceil(percentage_to_ranged_value(range, 66)) == 4 - assert math.ceil(percentage_to_ranged_value(range, 83)) == 5 - assert math.ceil(percentage_to_ranged_value(range, 100)) == 6 + assert math.ceil(percentage_to_ranged_value(value_range, 16)) == 1 + assert math.ceil(percentage_to_ranged_value(value_range, 33)) == 2 + assert math.ceil(percentage_to_ranged_value(value_range, 50)) == 3 + assert math.ceil(percentage_to_ranged_value(value_range, 66)) == 4 + assert math.ceil(percentage_to_ranged_value(value_range, 83)) == 5 + assert math.ceil(percentage_to_ranged_value(value_range, 100)) == 6 async def test_ranged_value_to_percentage_starting_at_one() -> None: """Test a range that starts with 1.""" - range = (1, 4) + value_range = (1, 4) - assert ranged_value_to_percentage(range, 1) == 25 - assert ranged_value_to_percentage(range, 2) == 50 - assert ranged_value_to_percentage(range, 3) == 75 - assert ranged_value_to_percentage(range, 4) == 100 + assert ranged_value_to_percentage(value_range, 1) == 25 + assert ranged_value_to_percentage(value_range, 2) == 50 + assert ranged_value_to_percentage(value_range, 3) == 75 + assert ranged_value_to_percentage(value_range, 4) == 100 async def test_ranged_value_to_percentage_starting_high() -> None: """Test a range that does not start with 1.""" - range = (101, 255) + value_range = (101, 255) - assert ranged_value_to_percentage(range, 101) == 0 - assert ranged_value_to_percentage(range, 139) == 25 - assert ranged_value_to_percentage(range, 178) == 50 - assert ranged_value_to_percentage(range, 217) == 75 - assert ranged_value_to_percentage(range, 255) == 100 + assert ranged_value_to_percentage(value_range, 101) == 0 + assert ranged_value_to_percentage(value_range, 139) == 25 + assert ranged_value_to_percentage(value_range, 178) == 50 + assert ranged_value_to_percentage(value_range, 217) == 75 + assert ranged_value_to_percentage(value_range, 255) == 100 async def test_ranged_value_to_percentage_starting_zero() -> None: """Test a range that starts with 0.""" - range = (0, 3) + value_range = (0, 3) - assert ranged_value_to_percentage(range, 0) == 25 - assert ranged_value_to_percentage(range, 1) == 50 - assert ranged_value_to_percentage(range, 2) == 75 - assert ranged_value_to_percentage(range, 3) == 100 + assert ranged_value_to_percentage(value_range, 0) == 25 + assert ranged_value_to_percentage(value_range, 1) == 50 + assert ranged_value_to_percentage(value_range, 2) == 75 + assert ranged_value_to_percentage(value_range, 3) == 100 diff --git a/tests/util/test_ssl.py b/tests/util/test_ssl.py index 4a88e061cbc..d0c7ce3bfb6 100644 --- a/tests/util/test_ssl.py +++ b/tests/util/test_ssl.py @@ -15,8 +15,7 @@ from homeassistant.util.ssl import ( @pytest.fixture def mock_sslcontext(): """Mock the ssl lib.""" - ssl_mock = MagicMock(set_ciphers=Mock(return_value=True)) - return ssl_mock + return MagicMock(set_ciphers=Mock(return_value=True)) def test_client_context(mock_sslcontext) -> None: diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index dba8e9b8017..f17489e1488 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -16,7 +16,7 @@ import yaml as pyyaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import homeassistant.util.yaml as yaml +from homeassistant.util import yaml from homeassistant.util.yaml import loader as yaml_loader from tests.common import extract_stack_to_frame, get_test_config_dir, patch_yaml_files @@ -568,13 +568,13 @@ def test_no_recursive_secrets( def test_input_class() -> None: """Test input class.""" - input = yaml_loader.Input("hello") - input2 = yaml_loader.Input("hello") + yaml_input = yaml_loader.Input("hello") + yaml_input2 = yaml_loader.Input("hello") - assert input.name == "hello" - assert input == input2 + assert yaml_input.name == "hello" + assert yaml_input == yaml_input2 - assert len({input, input2}) == 1 + assert len({yaml_input, yaml_input2}) == 1 def test_input(try_both_loaders, try_both_dumpers) -> None: